Repository: alchemyplatform/aa-sdk
Pull request #1542: feat: add mfa methods to rn-signer
Author: login: blakecduncan
Created: 2025-04-16T13:54:24Z | Merged: 2025-04-21T22:19:32Z

Tested these changes on a separate branch in the example expo app.  See the PR [here](https://github.com/alchemyplatform/aa-sdk/pull/1544)

# Pull Request Checklist

- [ ] Did you add new tests and confirm existing tests pass? (`yarn test`)
- [ ] Did you update relevant docs? (docs are found in the `site` folder, and guidelines for updating/adding docs can be found in the [contribution guide](https://github.com/alchemyplatform/aa-sdk/blob/main/CONTRIBUTING.md))
- [ ] Do your commits follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard?
- [ ] Does your PR title also follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard?
- [ ] If you have a breaking change, is it [correctly reflected in your commit message](https://www.conventionalcommits.org/en/v1.0.0/#examples)? (e.g. `feat!: breaking change`)
- [ ] Did you run lint (`yarn lint:check`) and fix any issues? (`yarn lint:write`)
- [ ] Did you follow the [contribution guidelines](https://github.com/alchemyplatform/aa-sdk/blob/main/CONTRIBUTING.md)?

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on refactoring the multi-factor authentication (MFA) functionality within the codebase, including renaming parameters and types, removing unused methods, and enhancing error handling.

### Detailed summary
- Deleted several unused `.mdx` files related to MFA methods.
- Renamed `EnableMfaParams` to `AddMfaParams` and `EnableMfaResult` to `AddMfaResult`.
- Updated return types for methods to reflect the new naming.
- Added a new `parseMfaError` function for better error handling.
- Implemented MFA methods in `BaseSignerClient` for listing, adding, verifying, and removing MFA factors.
- Removed deprecated methods from `RNSignerClient` and `AlchemySignerWebClient`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->


Diff:
diff --git a/account-kit/react/src/hooks/useMFA.ts b/account-kit/react/src/hooks/useMFA.ts
index 88a86f3265..c03fea8c17 100644
--- a/account-kit/react/src/hooks/useMFA.ts
+++ b/account-kit/react/src/hooks/useMFA.ts
@@ -5,15 +5,15 @@ import { useAlchemyAccountContext } from "./useAlchemyAccountContext.js";
 import { useSigner } from "./useSigner.js";
 import { useSignerStatus } from "./useSignerStatus.js";
 import type {
-  EnableMfaParams,
-  EnableMfaResult,
+  AddMfaParams,
+  AddMfaResult,
   MfaFactor,
   RemoveMfaParams,
   VerifyMfaParams,
 } from "@account-kit/signer";
 
 export type UseMFAResult = {
-  addMFA: UseMutationResult<EnableMfaResult, Error, EnableMfaParams>;
+  addMFA: UseMutationResult<AddMfaResult, Error, AddMfaParams>;
   verifyMFA: UseMutationResult<
     { multiFactors: MfaFactor[] },
     Error,
@@ -62,7 +62,7 @@ export function useMFA(): UseMFAResult {
     }
   };
 
-  const addMFA = useMutation<EnableMfaResult, Error, EnableMfaParams>(
+  const addMFA = useMutation<AddMfaResult, Error, AddMfaParams>(
     {
       mutationKey: ["addMFA"],
       mutationFn: async (params) => {
diff --git a/account-kit/rn-signer/src/client.ts b/account-kit/rn-signer/src/client.ts
index a3c4504feb..b618404cad 100644
--- a/account-kit/rn-signer/src/client.ts
+++ b/account-kit/rn-signer/src/client.ts
@@ -6,13 +6,11 @@ import { type ConnectionConfig } from "@aa-sdk/core";
 import {
   BaseSignerClient,
   OauthFailedError,
+  MfaRequiredError,
   type AlchemySignerClientEvents,
   type AuthenticatingEventMetadata,
   type CreateAccountParams,
-  type RemoveMfaParams,
   type EmailAuthParams,
-  type EnableMfaParams,
-  type EnableMfaResult,
   type GetWebAuthnAttestationResult,
   type MfaFactor,
   type OauthConfig,
@@ -20,7 +18,6 @@ import {
   type OtpParams,
   type SignupResponse,
   type User,
-  type VerifyMfaParams,
   type SubmitOtpCodeResponse,
 } from "@account-kit/signer";
 import { InAppBrowser } from "react-native-inappbrowser-reborn";
@@ -28,6 +25,7 @@ import { z } from "zod";
 import { InAppBrowserUnavailableError } from "./errors";
 import NativeTEKStamper from "./NativeTEKStamper.js";
 import { parseSearchParams } from "./utils/parseUrlParams";
+import { parseMfaError } from "./utils/parseMfaError";
 
 export const RNSignerClientParamsSchema = z.object({
   connection: z.custom<ConnectionConfig>(),
@@ -74,11 +72,30 @@ export class RNSignerClient extends BaseSignerClient<undefined> {
       targetPublicKey: publicKey,
     });
 
-    if (response.status === "MFA_REQUIRED") {
-      throw new Error("Multi-factor authentication is required");
+    if ("credentialBundle" in response && response.credentialBundle) {
+      return {
+        mfaRequired: false,
+        bundle: response.credentialBundle,
+      };
     }
 
-    return { bundle: response.credentialBundle, mfaRequired: false };
+    // If the server says "MFA_REQUIRED", pass that data back to the caller:
+    if (
+      response.status === "MFA_REQUIRED" &&
+      response.encryptedPayload &&
+      response.multiFactors
+    ) {
+      return {
+        mfaRequired: true,
+        encryptedPayload: response.encryptedPayload,
+        multiFactors: response.multiFactors,
+      };
+    }
+
+    // Otherwise, it's truly an error:
+    throw new Error(
+      "Failed to submit OTP code. Server did not return required fields."
+    );
   }
 
   override async createAccount(
@@ -105,17 +122,27 @@ export class RNSignerClient extends BaseSignerClient<undefined> {
 
   override async initEmailAuth(
     params: Omit<EmailAuthParams, "targetPublicKey">
-  ): Promise<{ orgId: string }> {
+  ): Promise<{ orgId: string; otpId?: string; multiFactors?: MfaFactor[] }> {
     this.eventEmitter.emit("authenticating", { type: "email" });
-    let targetPublicKey = await this.stamper.init();
-
-    const response = await this.request("/v1/auth", {
-      email: params.email,
-      emailMode: params.emailMode,
-      targetPublicKey,
-    });
+    const targetPublicKey = await this.stamper.init();
+
+    try {
+      return await this.request("/v1/auth", {
+        email: params.email,
+        emailMode: params.emailMode,
+        targetPublicKey,
+        multiFactors: params.multiFactors,
+      });
+    } catch (error) {
+      const multiFactors = parseMfaError(error);
 
-    return response;
+      // If MFA is required, and emailMode is Magic Link, the user must submit mfa with the request or
+      // the server will return an error with the required mfa factors.
+      if (multiFactors) {
+        throw new MfaRequiredError(multiFactors);
+      }
+      throw error;
+    }
   }
 
   override async completeAuthWithBundle(params: {
@@ -240,56 +267,4 @@ export class RNSignerClient extends BaseSignerClient<undefined> {
     const nonce = this.getOauthNonce(publicKey);
     return this.request("/v1/prepare-oauth", { nonce });
   };
-
-  /**
-   * Retrieves the list of MFA factors configured for the current user.
-   *
-   * @throws {Error} This method is not implemented in RNSignerClient
-   */
-  public override getMfaFactors(): Promise<{ multiFactors: MfaFactor[] }> {
-    throw new Error("getMfaFactors is not implemented in RNSignerClient");
-  }
-
-  /**
-   * Initiates the setup of a new MFA factor for the current user.
-   *
-   * @param {EnableMfaParams} _params The parameters required to enable a new MFA factor
-   * @throws {Error} This method is not implemented in RNSignerClient
-   */
-  public override addMfa(_params: EnableMfaParams): Promise<EnableMfaResult> {
-    throw new Error("enableMfa is not implemented in RNSignerClient");
-  }
-
-  /**
-   * Verifies a newly created MFA factor to complete the setup process.
-   *
-   * @param {VerifyMfaParams} _params The parameters required to verify the MFA factor
-   * @throws {Error} This method is not implemented in RNSignerClient
-   */
-  public override verifyMfa(_params: VerifyMfaParams): Promise<{
-    multiFactors: MfaFactor[];
-  }> {
-    throw new Error("verifyMfa is not implemented in RNSignerClient");
-  }
-
-  /**
-   * Removes existing MFA factors by ID.
-   *
-   * @param {RemoveMfaParams} _params The parameters specifying which factors to disable
-   * @throws {Error} This method is not implemented in RNSignerClient
-   */
-  public override removeMfa(_params: RemoveMfaParams): Promise<{
-    multiFactors: MfaFactor[];
-  }> {
-    throw new Error("disableMfa is not implemented in RNSignerClient");
-  }
-
-  public override validateMultiFactors(_params: {
-    encryptedPayload: string;
-    multiFactors: { multiFactorId: string; multiFactorCode: string }[];
-  }): Promise<{ bundle: string }> {
-    throw new Error(
-      "validateMultiFactors is not implemented in RNSignerClient"
-    );
-  }
 }
diff --git a/account-kit/rn-signer/src/utils/parseMfaError.ts b/account-kit/rn-signer/src/utils/parseMfaError.ts
new file mode 100644
index 0000000000..ca0397505b
--- /dev/null
+++ b/account-kit/rn-signer/src/utils/parseMfaError.ts
@@ -0,0 +1,15 @@
+import type { MfaFactor } from "@account-kit/signer";
+
+export function parseMfaError(error: unknown): MfaFactor[] | null {
+  if (error instanceof Error) {
+    try {
+      const parsed = JSON.parse(error.message);
+      if (parsed?.data?.multiFactors) {
+        return parsed.data.multiFactors;
+      }
+    } catch {
+      // ignore JSON parse failures
+    }
+  }
+  return null;
+}
diff --git a/account-kit/signer/src/base.ts b/account-kit/signer/src/base.ts
index 108633c5af..9961b73ecb 100644
--- a/account-kit/signer/src/base.ts
+++ b/account-kit/signer/src/base.ts
@@ -28,8 +28,8 @@ import type {
   OauthParams,
   User,
   VerifyMfaParams,
-  EnableMfaParams,
-  EnableMfaResult,
+  AddMfaParams,
+  AddMfaResult,
   RemoveMfaParams,
 } from "./client/types";
 import { NotAuthenticatedError } from "./errors.js";
@@ -1214,11 +1214,11 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
    * // Result contains multiFactorTotpUrl to display as QR code
    * ```
    *
-   * @param {EnableMfaParams} params The parameters required to enable a new MFA factor
+   * @param {AddMfaParams} params The parameters required to enable a new MFA factor
    * @throws {NotAuthenticatedError} If no user is authenticated
-   * @returns {Promise<EnableMfaResult>} A promise that resolves to the factor setup information
+   * @returns {Promise<AddMfaResult>} A promise that resolves to the factor setup information
    */
-  addMfa: (params: EnableMfaParams) => Promise<EnableMfaResult> =
+  addMfa: (params: AddMfaParams) => Promise<AddMfaResult> =
     SignerLogger.profiled("BaseAlchemySigner.addMfa", async (params) => {
       return this.inner.addMfa(params);
     });
diff --git a/account-kit/signer/src/client/base.ts b/account-kit/signer/src/client/base.ts
index 1ddba9493a..bc67ad82a7 100644
--- a/account-kit/signer/src/client/base.ts
+++ b/account-kit/signer/src/client/base.ts
@@ -16,8 +16,8 @@ import type {
   CreateAccountParams,
   RemoveMfaParams,
   EmailAuthParams,
-  EnableMfaParams,
-  EnableMfaResult,
+  AddMfaParams,
+  AddMfaResult,
   experimental_CreateApiKeyParams,
   GetOauthProviderUrlArgs,
   GetWebAuthnAttestationResult,
@@ -50,6 +50,14 @@ export type ExportWalletStamper = TurnkeyClient["stamper"] & {
   publicKey(): string | null;
 };
 
+const MFA_PAYLOAD = {
+  GET: "get_mfa",
+  ADD: "add_mfa",
+  DELETE: "delete_mfas",
+  VERIFY: "verify_mfa",
+  LIST: "list_mfas",
+} as const;
+
 /**
  * Base class for all Alchemy Signer clients
  */
@@ -142,53 +150,6 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
     params: Omit<EmailAuthParams, "targetPublicKey">
   ): Promise<{ orgId: string; otpId?: string; multiFactors?: MfaFactor[] }>;
 
-  /**
-   * Retrieves the list of MFA factors configured for the current user.
-   *
-   * @returns {Promise<{ multiFactors: Array<MfaFactor> }>} A promise that resolves to an array of configured MFA factors
-   */
-  public abstract getMfaFactors(): Promise<{
-    multiFactors: MfaFactor[];
-  }>;
-
-  /**
-   * Initiates the setup of a new MFA factor for the current user. Mfa will need to be verified before it is active.
-   *
-   * @param {EnableMfaParams} params The parameters required to enable a new MFA factor
-   * @returns {Promise<EnableMfaResult>} A promise that resolves to the factor setup information
-   */
-  public abstract addMfa(params: EnableMfaParams): Promise<EnableMfaResult>;
-
-  /**
-   * Verifies a newly created MFA factor to complete the setup process.
-   *
-   * @param {VerifyMfaParams} params The parameters required to verify the MFA factor
-   * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to the updated list of MFA factors
-   */
-  public abstract verifyMfa(params: VerifyMfaParams): Promise<{
-    multiFactors: MfaFactor[];
-  }>;
-
-  /**
-   * Removes existing MFA factors by ID or factor type.
-   *
-   * @param {RemoveMfaParams} params The parameters specifying which factors to disable
-   * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to the updated list of MFA factors
-   */
-  public abstract removeMfa(params: RemoveMfaParams): Promise<{
-    multiFactors: MfaFactor[];
-  }>;
-
-  /**
-   * Validates multiple MFA factors using the provided encrypted payload and MFA codes.
-   *
-   * @param {ValidateMultiFactorsParams} params The validation parameters
-   * @returns {Promise<{ bundle: string }>} A promise that resolves to an object containing the credential bundle
-   */
-  public abstract validateMultiFactors(
-    params: ValidateMultiFactorsParams
-  ): Promise<{ bundle: string }>;
-
   public abstract completeAuthWithBundle(params: {
     bundle: string;
     orgId: string;
@@ -536,6 +497,167 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
     return json as SignerResponse<R>;
   };
 
+  /**
+   * Retrieves the list of MFA factors configured for the current user.
+   *
+   * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to an array of configured MFA factors
+   * @throws {NotAuthenticatedError} If no user is authenticated
+   */
+  public getMfaFactors = async (): Promise<{
+    multiFactors: MfaFactor[];
+  }> => {
+    if (!this.user) {
+      throw new NotAuthenticatedError();
+    }
+
+    const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
+      organizationId: this.user.orgId,
+      type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
+      timestampMs: Date.now().toString(),
+      parameters: {
+        encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
+        hashFunction: "HASH_FUNCTION_NO_OP",
+        payload: MFA_PAYLOAD.LIST,
+        signWith: this.user.address,
+      },
+    });
+
+    return this.request("/v1/auth-list-multi-factors", {
+      stampedRequest,
+    });
+  };
+
+  /**
+   * Initiates the setup of a new MFA factor for the current user. Mfa will need to be verified before it is active.
+   *
+   * @param {AddMfaParams} params The parameters required to enable a new MFA factor
+   * @returns {Promise<AddMfaResult>} A promise that resolves to the factor setup information
+   * @throws {NotAuthenticatedError} If no user is authenticated
+   * @throws {Error} If an unsupported factor type is provided
+   */
+  public addMfa = async (params: AddMfaParams): Promise<AddMfaResult> => {
+    if (!this.user) {
+      throw new NotAuthenticatedError();
+    }
+
+    const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
+      organizationId: this.user.orgId,
+      type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
+      timestampMs: Date.now().toString(),
+      parameters: {
+        encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
+        hashFunction: "HASH_FUNCTION_NO_OP",
+        payload: MFA_PAYLOAD.ADD,
+        signWith: this.user.address,
+      },
+    });
+
+    switch (params.multiFactorType) {
+      case "totp":
+        return this.request("/v1/auth-request-multi-factor", {
+          stampedRequest,
+          multiFactorType: params.multiFactorType,
+        });
+      default:
+        throw new Error(
+          `Unsupported MFA factor type: ${params.multiFactorType}`
+        );
+    }
+  };
+
+  /**
+   * Verifies a newly created MFA factor to complete the setup process.
+   *
+   * @param {VerifyMfaParams} params The parameters required to verify the MFA factor
+   * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to the updated list of MFA factors
+   * @throws {NotAuthenticatedError} If no user is authenticated
+   */
+  public verifyMfa = async (
+    params: VerifyMfaParams
+  ): Promise<{ multiFactors: MfaFactor[] }> => {
+    if (!this.user) {
+      throw new NotAuthenticatedError();
+    }
+
+    const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
+      organizationId: this.user.orgId,
+      type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
+      timestampMs: Date.now().toString(),
+      parameters: {
+        encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
+        hashFunction: "HASH_FUNCTION_NO_OP",
+        payload: MFA_PAYLOAD.VERIFY,
+        signWith: this.user.address,
+      },
+    });
+
+    return this.request("/v1/auth-verify-multi-factor", {
+      stampedRequest,
+      multiFactorId: params.multiFactorId,
+      multiFactorCode: params.multiFactorCode,
+    });
+  };
+
+  /**
+   * Removes existing MFA factors by ID.
+   *
+   * @param {RemoveMfaParams} params The parameters specifying which factors to disable
+   * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to the updated list of MFA factors
+   * @throws {NotAuthenticatedError} If no user is authenticated
+   */
+  public removeMfa = async (
+    params: RemoveMfaParams
+  ): Promise<{ multiFactors: MfaFactor[] }> => {
+    if (!this.user) {
+      throw new NotAuthenticatedError();
+    }
+
+    const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
+      organizationId: this.user.orgId,
+      type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
+      timestampMs: Date.now().toString(),
+      parameters: {
+        encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
+        hashFunction: "HASH_FUNCTION_NO_OP",
+        payload: MFA_PAYLOAD.DELETE,
+        signWith: this.user.address,
+      },
+    });
+
+    return this.request("/v1/auth-delete-multi-factors", {
+      stampedRequest,
+      multiFactorIds: params.multiFactorIds,
+    });
+  };
+
+  /**
+   * Validates multiple MFA factors using the provided encrypted payload and MFA codes.
+   *
+   * @param {ValidateMultiFactorsParams} params The validation parameters
+   * @returns {Promise<{ bundle: string }>} A promise that resolves to an object containing the credential bundle
+   * @throws {Error} If no credential bundle is returned from the server
+   */
+  public validateMultiFactors = async (
+    params: ValidateMultiFactorsParams
+  ): Promise<{ bundle: string }> => {
+    // Send the encryptedPayload plus TOTP codes, etc:
+    const response = await this.request("/v1/auth-validate-multi-factors", {
+      encryptedPayload: params.encryptedPayload,
+      multiFactors: params.multiFactors,
+    });
+
+    // The server is expected to return the *decrypted* payload in `response.payload.credentialBundle`
+    if (!response.payload || !response.payload.credentialBundle) {
+      throw new Error(
+        "Request to validateMultiFactors did not return a credential bundle"
+      );
+    }
+
+    return {
+      bundle: response.payload.credentialBundle,
+    };
+  };
+
   // #endregion
 
   // #region PRIVATE METHODS
diff --git a/account-kit/signer/src/client/index.ts b/account-kit/signer/src/client/index.ts
index 3e08e3452e..5eb1c8c0b9 100644
--- a/account-kit/signer/src/client/index.ts
+++ b/account-kit/signer/src/client/index.ts
@@ -17,25 +17,12 @@ import type {
   OauthConfig,
   OtpParams,
   User,
-  MfaFactor,
-  EnableMfaParams,
-  EnableMfaResult,
-  VerifyMfaParams,
-  RemoveMfaParams,
   SubmitOtpCodeResponse,
-  ValidateMultiFactorsParams,
 } from "./types.js";
-import { MfaRequiredError, NotAuthenticatedError } from "../errors.js";
+import { MfaRequiredError } from "../errors.js";
 import { parseMfaError } from "../utils/parseMfaError.js";
 
 const CHECK_CLOSE_INTERVAL = 500;
-const MFA_PAYLOAD = {
-  GET: "get_mfa",
-  ADD: "add_mfa",
-  DELETE: "delete_mfas",
-  VERIFY: "verify_mfa",
-  LIST: "list_mfas",
-};
 
 export const AlchemySignerClientParamsSchema = z.object({
   connection: ConnectionConfigSchema,
@@ -722,169 +709,6 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
     const nonce = this.getOauthNonce(publicKey);
     return this.request("/v1/prepare-oauth", { nonce });
   };
-
-  /**
-   * Retrieves the list of MFA factors configured for the current user.
-   *
-   * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to an array of configured MFA factors
-   * @throws {NotAuthenticatedError} If no user is authenticated
-   */
-  public override getMfaFactors = async (): Promise<{
-    multiFactors: MfaFactor[];
-  }> => {
-    if (!this.user) {
-      throw new NotAuthenticatedError();
-    }
-
-    const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
-      organizationId: this.user.orgId,
-      type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
-      timestampMs: Date.now().toString(),
-      parameters: {
-        encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
-        hashFunction: "HASH_FUNCTION_NO_OP",
-        payload: MFA_PAYLOAD.LIST,
-        signWith: this.user.address,
-      },
-    });
-
-    return this.request("/v1/auth-list-multi-factors", {
-      stampedRequest,
-    });
-  };
-
-  /**
-   * Initiates the setup of a new MFA factor for the current user. Mfa will need to be verified before it is active.
-   *
-   * @param {EnableMfaParams} params The parameters required to enable a new MFA factor
-   * @returns {Promise<EnableMfaResult>} A promise that resolves to the factor setup information
-   * @throws {NotAuthenticatedError} If no user is authenticated
-   * @throws {Error} If an unsupported factor type is provided
-   */
-  public override addMfa = async (
-    params: EnableMfaParams
-  ): Promise<EnableMfaResult> => {
-    if (!this.user) {
-      throw new NotAuthenticatedError();
-    }
-
-    const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
-      organizationId: this.user.orgId,
-      type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
-      timestampMs: Date.now().toString(),
-      parameters: {
-        encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
-        hashFunction: "HASH_FUNCTION_NO_OP",
-        payload: MFA_PAYLOAD.ADD,
-        signWith: this.user.address,
-      },
-    });
-
-    switch (params.multiFactorType) {
-      case "totp":
-        return this.request("/v1/auth-request-multi-factor", {
-          stampedRequest,
-          multiFactorType: params.multiFactorType,
-        });
-      default:
-        throw new Error(
-          `Unsupported MFA factor type: ${params.multiFactorType}`
-        );
-    }
-  };
-
-  /**
-   * Verifies a newly created MFA factor to complete the setup process.
-   *
-   * @param {VerifyMfaParams} params The parameters required to verify the MFA factor
-   * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to the updated list of MFA factors
-   * @throws {NotAuthenticatedError} If no user is authenticated
-   */
-  public override verifyMfa = async (
-    params: VerifyMfaParams
-  ): Promise<{ multiFactors: MfaFactor[] }> => {
-    if (!this.user) {
-      throw new NotAuthenticatedError();
-    }
-
-    const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
-      organizationId: this.user.orgId,
-      type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
-      timestampMs: Date.now().toString(),
-      parameters: {
-        encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
-        hashFunction: "HASH_FUNCTION_NO_OP",
-        payload: MFA_PAYLOAD.VERIFY,
-        signWith: this.user.address,
-      },
-    });
-
-    return this.request("/v1/auth-verify-multi-factor", {
-      stampedRequest,
-      multiFactorId: params.multiFactorId,
-      multiFactorCode: params.multiFactorCode,
-    });
-  };
-
-  /**
-   * Removes existing MFA factors by ID.
-   *
-   * @param {RemoveMfaParams} params The parameters specifying which factors to disable
-   * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to the updated list of MFA factors
-   * @throws {NotAuthenticatedError} If no user is authenticated
-   */
-  public override removeMfa = async (
-    params: RemoveMfaParams
-  ): Promise<{ multiFactors: MfaFactor[] }> => {
-    if (!this.user) {
-      throw new NotAuthenticatedError();
-    }
-
-    const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
-      organizationId: this.user.orgId,
-      type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
-      timestampMs: Date.now().toString(),
-      parameters: {
-        encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
-        hashFunction: "HASH_FUNCTION_NO_OP",
-        payload: MFA_PAYLOAD.DELETE,
-        signWith: this.user.address,
-      },
-    });
-
-    return this.request("/v1/auth-delete-multi-factors", {
-      stampedRequest,
-      multiFactorIds: params.multiFactorIds,
-    });
-  };
-
-  /**
-   * Validates multiple MFA factors using the provided encrypted payload and MFA codes.
-   *
-   * @param {ValidateMultiFactorsParams} params The validation parameters
-   * @returns {Promise<{ bundle: string }>} A promise that resolves to an object containing the credential bundle
-   * @throws {Error} If no credential bundle is returned from the server
-   */
-  public override async validateMultiFactors(
-    params: ValidateMultiFactorsParams
-  ): Promise<{ bundle: string }> {
-    // Send the encryptedPayload plus TOTP codes, etc:
-    const response = await this.request("/v1/auth-validate-multi-factors", {
-      encryptedPayload: params.encryptedPayload,
-      multiFactors: params.multiFactors,
-    });
-
-    // The server is expected to return the *decrypted* payload in `response.payload.credentialBundle`
-    if (!response.payload || !response.payload.credentialBundle) {
-      throw new Error(
-        "Request to validateMultiFactors did not return a credential bundle"
-      );
-    }
-
-    return {
-      bundle: response.payload.credentialBundle,
-    };
-  }
 }
 
 /**
diff --git a/account-kit/signer/src/client/types.ts b/account-kit/signer/src/client/types.ts
index 3e07598188..ebf67c73d3 100644
--- a/account-kit/signer/src/client/types.ts
+++ b/account-kit/signer/src/client/types.ts
@@ -208,7 +208,7 @@ export type SignerEndpoints = [
       stampedRequest: TSignedRequest;
       multiFactorType: MultiFactorType;
     };
-    Response: EnableMfaResult;
+    Response: AddMfaResult;
   },
   {
     Route: "/v1/auth-verify-multi-factor";
@@ -287,11 +287,11 @@ export type MfaFactor = {
 
 type MultiFactorType = "totp";
 
-export type EnableMfaParams = {
+export type AddMfaParams = {
   multiFactorType: MultiFactorType;
 };
 
-export type EnableMfaResult = {
+export type AddMfaResult = {
   multiFactorType: MultiFactorType;
   multiFactorId: string;
   multiFactorTotpUrl: string;
@@ -308,10 +308,7 @@ export type RemoveMfaParams = {
 
 export type ValidateMultiFactorsParams = {
   encryptedPayload: string;
-  multiFactors: Array<{
-    multiFactorId: string;
-    multiFactorCode: string;
-  }>;
+  multiFactors: VerifyMfaParams[];
 };
 
 export type MfaChallenge = {
diff --git a/site/pages/reference/account-kit/react-native-signer/classes/RNSignerClient/addMfa.mdx b/site/pages/reference/account-kit/react-native-signer/classes/RNSignerClient/addMfa.mdx
deleted file mode 100644
index bd416e3e1e..0000000000
--- a/site/pages/reference/account-kit/react-native-signer/classes/RNSignerClient/addMfa.mdx
+++ /dev/null
@@ -1,22 +0,0 @@
----
-# This file is autogenerated
-title: addMfa
-description: Overview of the addMfa method
----
-
-# addMfa
-
-Initiates the setup of a new MFA factor for the current user.
-
-## Import
-
-```ts
-import { RNSignerClient } from "@account-kit/react-native-signer";
-```
-
-## Parameters
-
-### \_params
-
-`EnableMfaParams`
-The parameters required to enable a new MFA factor
diff --git a/site/pages/reference/account-kit/react-native-signer/classes/RNSignerClient/getMfaFactors.mdx b/site/pages/reference/account-kit/react-native-signer/classes/RNSignerClient/getMfaFactors.mdx
deleted file mode 100644
index ff8f3d55aa..0000000000
--- a/site/pages/reference/account-kit/react-native-signer/classes/RNSignerClient/getMfaFactors.mdx
+++ /dev/null
@@ -1,15 +0,0 @@
----
-# This file is autogenerated
-title: getMfaFactors
-description: Overview of the getMfaFactors method
----
-
-# getMfaFactors
-
-Retrieves the list of MFA factors configured for the current user.
-
-## Import
-
-```ts
-import { RNSignerClient } from "@account-kit/react-native-signer";
-```
diff --git a/site/pages/reference/account-kit/react-native-signer/classes/RNSignerClient/removeMfa.mdx b/site/pages/reference/account-kit/react-native-signer/classes/RNSignerClient/removeMfa.mdx
deleted file mode 100644
index 34c6fa0e4f..0000000000
--- a/site/pages/reference/account-kit/react-native-signer/classes/RNSignerClient/removeMfa.mdx
+++ /dev/null
@@ -1,22 +0,0 @@
----
-# This file is autogenerated
-title: removeMfa
-description: Overview of the removeMfa method
----
-
-# removeMfa
-
-Removes existing MFA factors by ID.
-
-## Import
-
-```ts
-import { RNSignerClient } from "@account-kit/react-native-signer";
-```
-
-## Parameters
-
-### \_params
-
-`RemoveMfaParams`
-The parameters specifying which factors to disable
diff --git a/site/pages/reference/account-kit/react-native-signer/classes/RNSignerClient/verifyMfa.mdx b/site/pages/reference/account-kit/react-native-signer/classes/RNSignerClient/verifyMfa.mdx
deleted file mode 100644
index 28a6e34440..0000000000
--- a/site/pages/reference/account-kit/react-native-signer/classes/RNSignerClient/verifyMfa.mdx
+++ /dev/null
@@ -1,22 +0,0 @@
----
-# This file is autogenerated
-title: verifyMfa
-description: Overview of the verifyMfa method
----
-
-# verifyMfa
-
-Verifies a newly created MFA factor to complete the setup process.
-
-## Import
-
-```ts
-import { RNSignerClient } from "@account-kit/react-native-signer";
-```
-
-## Parameters
-
-### \_params
-
-`VerifyMfaParams`
-The parameters required to verify the MFA factor
diff --git a/site/pages/reference/account-kit/signer/classes/AlchemySignerWebClient/addMfa.mdx b/site/pages/reference/account-kit/signer/classes/AlchemySignerWebClient/addMfa.mdx
deleted file mode 100644
index 94c42e2355..0000000000
--- a/site/pages/reference/account-kit/signer/classes/AlchemySignerWebClient/addMfa.mdx
+++ /dev/null
@@ -1,27 +0,0 @@
----
-# This file is autogenerated
-title: addMfa
-description: Overview of the addMfa method
----
-
-# addMfa
-
-Initiates the setup of a new MFA factor for the current user. Mfa will need to be verified before it is active.
-
-## Import
-
-```ts
-import { AlchemySignerWebClient } from "@account-kit/signer";
-```
-
-## Parameters
-
-### params
-
-`EnableMfaParams`
-The parameters required to enable a new MFA factor
-
-## Returns
-
-`Promise<EnableMfaResult>`
-A promise that resolves to the factor setup information
diff --git a/site/pages/reference/account-kit/signer/classes/AlchemySignerWebClient/getMfaFactors.mdx b/site/pages/reference/account-kit/signer/classes/AlchemySignerWebClient/getMfaFactors.mdx
deleted file mode 100644
index 49a1d77bbe..0000000000
--- a/site/pages/reference/account-kit/signer/classes/AlchemySignerWebClient/getMfaFactors.mdx
+++ /dev/null
@@ -1,20 +0,0 @@
----
-# This file is autogenerated
-title: getMfaFactors
-description: Overview of the getMfaFactors method
----
-
-# getMfaFactors
-
-Retrieves the list of MFA factors configured for the current user.
-
-## Import
-
-```ts
-import { AlchemySignerWebClient } from "@account-kit/signer";
-```
-
-## Returns
-
-`Promise<{ multiFactors: MfaFactor[] }>`
-A promise that resolves to an array of configured MFA factors
diff --git a/site/pages/reference/account-kit/signer/classes/AlchemySignerWebClient/removeMfa.mdx b/site/pages/reference/account-kit/signer/classes/AlchemySignerWebClient/removeMfa.mdx
deleted file mode 100644
index e910b4f6b6..0000000000
--- a/site/pages/reference/account-kit/signer/classes/AlchemySignerWebClient/removeMfa.mdx
+++ /dev/null
@@ -1,27 +0,0 @@
----
-# This file is autogenerated
-title: removeMfa
-description: Overview of the removeMfa method
----
-
-# removeMfa
-
-Removes existing MFA factors by ID.
-
-## Import
-
-```ts
-import { AlchemySignerWebClient } from "@account-kit/signer";
-```
-
-## Parameters
-
-### params
-
-`RemoveMfaParams`
-The parameters specifying which factors to disable
-
-## Returns
-
-`Promise<{ multiFactors: MfaFactor[] }>`
-A promise that resolves to the updated list of MFA factors
diff --git a/site/pages/reference/account-kit/signer/classes/AlchemySignerWebClient/validateMultiFactors.mdx b/site/pages/reference/account-kit/signer/classes/AlchemySignerWebClient/validateMultiFactors.mdx
deleted file mode 100644
index ed0299cb6c..0000000000
--- a/site/pages/reference/account-kit/signer/classes/AlchemySignerWebClient/validateMultiFactors.mdx
+++ /dev/null
@@ -1,27 +0,0 @@
----
-# This file is autogenerated
-title: validateMultiFactors
-description: Overview of the validateMultiFactors method
----
-
-# validateMultiFactors
-
-Validates multiple MFA factors using the provided encrypted payload and MFA codes.
-
-## Import
-
-```ts
-import { AlchemySignerWebClient } from "@account-kit/signer";
-```
-
-## Parameters
-
-### params
-
-`ValidateMultiFactorsParams`
-The validation parameters
-
-## Returns
-
-`Promise<{ bundle: string }>`
-A promise that resolves to an object containing the credential bundle
diff --git a/site/pages/reference/account-kit/signer/classes/AlchemySignerWebClient/verifyMfa.mdx b/site/pages/reference/account-kit/signer/classes/AlchemySignerWebClient/verifyMfa.mdx
deleted file mode 100644
index 7c5eadda7d..0000000000
--- a/site/pages/reference/account-kit/signer/classes/AlchemySignerWebClient/verifyMfa.mdx
+++ /dev/null
@@ -1,27 +0,0 @@
----
-# This file is autogenerated
-title: verifyMfa
-description: Overview of the verifyMfa method
----
-
-# verifyMfa
-
-Verifies a newly created MFA factor to complete the setup process.
-
-## Import
-
-```ts
-import { AlchemySignerWebClient } from "@account-kit/signer";
-```
-
-## Parameters
-
-### params
-
-`VerifyMfaParams`
-The parameters required to verify the MFA factor
-
-## Returns
-
-`Promise<{ multiFactors: MfaFactor[] }>`
-A promise that resolves to the updated list of MFA factors
diff --git a/site/pages/reference/account-kit/signer/classes/BaseAlchemySigner/addMfa.mdx b/site/pages/reference/account-kit/signer/classes/BaseAlchemySigner/addMfa.mdx
index fe56263ad7..8d734cd5c6 100644
--- a/site/pages/reference/account-kit/signer/classes/BaseAlchemySigner/addMfa.mdx
+++ b/site/pages/reference/account-kit/signer/classes/BaseAlchemySigner/addMfa.mdx
@@ -40,10 +40,10 @@ const result = await signer.addMfa({ multiFactorType: "totp" });
 
 ### params
 
-`EnableMfaParams`
+`AddMfaParams`
 The parameters required to enable a new MFA factor
 
 ## Returns
 
-`Promise<EnableMfaResult>`
+`Promise<AddMfaResult>`
 A promise that resolves to the factor setup information
diff --git a/site/pages/reference/account-kit/signer/classes/BaseSignerClient/addMfa.mdx b/site/pages/reference/account-kit/signer/classes/BaseSignerClient/addMfa.mdx
index 9659f1875c..aeb4c7b2bb 100644
--- a/site/pages/reference/account-kit/signer/classes/BaseSignerClient/addMfa.mdx
+++ b/site/pages/reference/account-kit/signer/classes/BaseSignerClient/addMfa.mdx
@@ -18,10 +18,10 @@ import { BaseSignerClient } from "@account-kit/signer";
 
 ### params
 
-`EnableMfaParams`
+`AddMfaParams`
 The parameters required to enable a new MFA factor
 
 ## Returns
 
-`Promise<EnableMfaResult>`
+`Promise<AddMfaResult>`
 A promise that resolves to the factor setup information
diff --git a/site/pages/reference/account-kit/signer/classes/BaseSignerClient/getMfaFactors.mdx b/site/pages/reference/account-kit/signer/classes/BaseSignerClient/getMfaFactors.mdx
index 251eb6c47b..157e9311bf 100644
--- a/site/pages/reference/account-kit/signer/classes/BaseSignerClient/getMfaFactors.mdx
+++ b/site/pages/reference/account-kit/signer/classes/BaseSignerClient/getMfaFactors.mdx
@@ -16,5 +16,5 @@ import { BaseSignerClient } from "@account-kit/signer";
 
 ## Returns
 
-`Promise<{ multiFactors: Array<MfaFactor> }>`
+`Promise<{ multiFactors: MfaFactor[] }>`
 A promise that resolves to an array of configured MFA factors
diff --git a/site/pages/reference/account-kit/signer/classes/BaseSignerClient/removeMfa.mdx b/site/pages/reference/account-kit/signer/classes/BaseSignerClient/removeMfa.mdx
index b1a9e17cee..e5e0dfd4d9 100644
--- a/site/pages/reference/account-kit/signer/classes/BaseSignerClient/removeMfa.mdx
+++ b/site/pages/reference/account-kit/signer/classes/BaseSignerClient/removeMfa.mdx
@@ -6,7 +6,7 @@ description: Overview of the removeMfa method
 
 # removeMfa
 
-Removes existing MFA factors by ID or factor type.
+Removes existing MFA factors by ID.
 
 ## Import
 


Reviews:
user: login: Blu-J
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T20:51:57Z
user: login: Blu-J
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T20:56:50Z
user: login: blakecduncan
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T17:17:09Z
user: login: moldy530
body: general comment is that it feels like most of the MFA methods don't depend on environment specific things, so they should be defined in the base class rather than per client definition
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T15:44:28Z
user: login: jakehobbs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T15:49:01Z
user: login: moldy530
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T16:10:07Z
user: login: blakecduncan
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T16:19:52Z
user: login: jakehobbs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T16:22:25Z
user: login: moldy530
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T20:08:28Z
user: login: moldy530
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T22:05:14Z

Review comments:
diff_hunk: @@ -28,6 +30,15 @@ import { z } from "zod";
 import { InAppBrowserUnavailableError } from "./errors";
 import NativeTEKStamper from "./NativeTEKStamper.js";
 import { parseSearchParams } from "./utils/parseUrlParams";
+import { parseMfaError } from "./utils/parseMfaError";
+
+const MFA_PAYLOAD = {
+  GET: "get_mfa",
+  ADD: "add_mfa",
+  DELETE: "delete_mfas",
+  VERIFY: "verify_mfa",
+  LIST: "list_mfas",
+};
path: account-kit/rn-signer/src/client.ts
user: login: Blu-J
body: This will let the const be the exact type, not just GET: string

```suggestion
} as const;
```
created_at: 2025-04-16T20:51:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,15 @@
+import type { MfaFactor } from "@account-kit/signer";
+
+export function parseMfaError(error: unknown): MfaFactor[] | null {
+  if (error instanceof Error) {
+    try {
+      const parsed = JSON.parse(error.message);
+      if (parsed?.data?.multiFactors) {
+        return parsed.data.multiFactors;
+      }
+    } catch {
+      // ignore JSON parse failures
path: account-kit/rn-signer/src/utils/parseMfaError.ts
user: login: Blu-J
body: ❓ Why are we ignoring it, I guess if we want to debug this will be findable in the network right?
created_at: 2025-04-16T20:56:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,15 @@
+import type { MfaFactor } from "@account-kit/signer";
+
+export function parseMfaError(error: unknown): MfaFactor[] | null {
+  if (error instanceof Error) {
+    try {
+      const parsed = JSON.parse(error.message);
+      if (parsed?.data?.multiFactors) {
+        return parsed.data.multiFactors;
+      }
+    } catch {
+      // ignore JSON parse failures
path: account-kit/rn-signer/src/utils/parseMfaError.ts
user: login: blakecduncan
body: Yeah good question, json.parse throws an error if you pass it invalid json data.  I am assuming here if we are passing invalid json, it's not an mfa error.  In that case, we should return null and let the calling function handle the error
created_at: 2025-04-17T17:17:09Z
author_association: COLLABORATOR
diff_hunk: @@ -244,52 +284,162 @@ export class RNSignerClient extends BaseSignerClient<undefined> {
   /**
    * Retrieves the list of MFA factors configured for the current user.
    *
-   * @throws {Error} This method is not implemented in RNSignerClient
+   * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to an array of configured MFA factors
+   * @throws {Error} If no user is authenticated
    */
-  public override getMfaFactors(): Promise<{ multiFactors: MfaFactor[] }> {
-    throw new Error("getMfaFactors is not implemented in RNSignerClient");
-  }
+  public override getMfaFactors = async (): Promise<{
path: account-kit/rn-signer/src/client.ts
user: login: moldy530
body: shouldn't the base implementation just work here?
created_at: 2025-04-21T15:42:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -244,52 +284,162 @@ export class RNSignerClient extends BaseSignerClient<undefined> {
   /**
    * Retrieves the list of MFA factors configured for the current user.
    *
-   * @throws {Error} This method is not implemented in RNSignerClient
+   * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to an array of configured MFA factors
+   * @throws {Error} If no user is authenticated
    */
-  public override getMfaFactors(): Promise<{ multiFactors: MfaFactor[] }> {
-    throw new Error("getMfaFactors is not implemented in RNSignerClient");
-  }
+  public override getMfaFactors = async (): Promise<{
+    multiFactors: MfaFactor[];
+  }> => {
+    if (!this.user) {
+      throw new Error("Not authenticated");
+    }
+
+    const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
+      organizationId: this.user.orgId,
+      type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
+      timestampMs: Date.now().toString(),
+      parameters: {
+        encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
+        hashFunction: "HASH_FUNCTION_NO_OP",
+        payload: MFA_PAYLOAD.LIST,
+        signWith: this.user.address,
+      },
+    });
+
+    return this.request("/v1/auth-list-multi-factors", {
+      stampedRequest,
+    });
+  };
 
   /**
    * Initiates the setup of a new MFA factor for the current user.
    *
-   * @param {EnableMfaParams} _params The parameters required to enable a new MFA factor
-   * @throws {Error} This method is not implemented in RNSignerClient
+   * @param {EnableMfaParams} params The parameters required to enable a new MFA factor
+   * @returns {Promise<EnableMfaResult>} A promise that resolves to the factor setup information
+   * @throws {Error} If no user is authenticated or if an unsupported factor type is provided
    */
-  public override addMfa(_params: EnableMfaParams): Promise<EnableMfaResult> {
-    throw new Error("enableMfa is not implemented in RNSignerClient");
-  }
+  public override addMfa = async (
path: account-kit/rn-signer/src/client.ts
user: login: moldy530
body: same here, shouldn't this just be handled by the base implementation? 
created_at: 2025-04-21T15:42:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -244,52 +284,162 @@ export class RNSignerClient extends BaseSignerClient<undefined> {
   /**
    * Retrieves the list of MFA factors configured for the current user.
    *
-   * @throws {Error} This method is not implemented in RNSignerClient
+   * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to an array of configured MFA factors
+   * @throws {Error} If no user is authenticated
    */
-  public override getMfaFactors(): Promise<{ multiFactors: MfaFactor[] }> {
-    throw new Error("getMfaFactors is not implemented in RNSignerClient");
-  }
+  public override getMfaFactors = async (): Promise<{
+    multiFactors: MfaFactor[];
+  }> => {
+    if (!this.user) {
+      throw new Error("Not authenticated");
+    }
+
+    const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
+      organizationId: this.user.orgId,
+      type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
+      timestampMs: Date.now().toString(),
+      parameters: {
+        encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
+        hashFunction: "HASH_FUNCTION_NO_OP",
+        payload: MFA_PAYLOAD.LIST,
+        signWith: this.user.address,
+      },
+    });
+
+    return this.request("/v1/auth-list-multi-factors", {
+      stampedRequest,
+    });
+  };
 
   /**
    * Initiates the setup of a new MFA factor for the current user.
    *
-   * @param {EnableMfaParams} _params The parameters required to enable a new MFA factor
-   * @throws {Error} This method is not implemented in RNSignerClient
+   * @param {EnableMfaParams} params The parameters required to enable a new MFA factor
+   * @returns {Promise<EnableMfaResult>} A promise that resolves to the factor setup information
+   * @throws {Error} If no user is authenticated or if an unsupported factor type is provided
    */
-  public override addMfa(_params: EnableMfaParams): Promise<EnableMfaResult> {
-    throw new Error("enableMfa is not implemented in RNSignerClient");
-  }
+  public override addMfa = async (
+    params: EnableMfaParams
+  ): Promise<EnableMfaResult> => {
+    if (!this.user) {
+      throw new Error("Not authenticated");
+    }
+
+    const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
+      organizationId: this.user.orgId,
+      type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
+      timestampMs: Date.now().toString(),
+      parameters: {
+        encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
+        hashFunction: "HASH_FUNCTION_NO_OP",
+        payload: MFA_PAYLOAD.ADD,
+        signWith: this.user.address,
+      },
+    });
+
+    switch (params.multiFactorType) {
+      case "totp":
+        return this.request("/v1/auth-request-multi-factor", {
+          stampedRequest,
+          multiFactorType: params.multiFactorType,
+        });
+      default:
+        throw new Error(
+          `Unsupported MFA factor type: ${params.multiFactorType}`
+        );
+    }
+  };
 
   /**
    * Verifies a newly created MFA factor to complete the setup process.
    *
-   * @param {VerifyMfaParams} _params The parameters required to verify the MFA factor
-   * @throws {Error} This method is not implemented in RNSignerClient
+   * @param {VerifyMfaParams} params The parameters required to verify the MFA factor
+   * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to the updated list of MFA factors
+   * @throws {Error} If no user is authenticated
    */
-  public override verifyMfa(_params: VerifyMfaParams): Promise<{
-    multiFactors: MfaFactor[];
-  }> {
-    throw new Error("verifyMfa is not implemented in RNSignerClient");
-  }
+  public override verifyMfa = async (
path: account-kit/rn-signer/src/client.ts
user: login: moldy530
body: here as well, I can't recall but `this.turnkeyClient` should be available from the base impl which means that all of the code in here isn't specific to the environment that it's running in
created_at: 2025-04-21T15:43:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -244,52 +284,162 @@ export class RNSignerClient extends BaseSignerClient<undefined> {
   /**
    * Retrieves the list of MFA factors configured for the current user.
    *
-   * @throws {Error} This method is not implemented in RNSignerClient
+   * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to an array of configured MFA factors
+   * @throws {Error} If no user is authenticated
    */
-  public override getMfaFactors(): Promise<{ multiFactors: MfaFactor[] }> {
-    throw new Error("getMfaFactors is not implemented in RNSignerClient");
-  }
+  public override getMfaFactors = async (): Promise<{
path: account-kit/rn-signer/src/client.ts
user: login: jakehobbs
body: i had this question while working on my "api key signer" client impl as well... the MFA methods are all abstract on the `BaseSignerClient`, unlike many of the other methods (whoami, signRawMessage, etc.)
created_at: 2025-04-21T15:49:01Z
author_association: COLLABORATOR
diff_hunk: @@ -244,52 +284,162 @@ export class RNSignerClient extends BaseSignerClient<undefined> {
   /**
    * Retrieves the list of MFA factors configured for the current user.
    *
-   * @throws {Error} This method is not implemented in RNSignerClient
+   * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to an array of configured MFA factors
+   * @throws {Error} If no user is authenticated
    */
-  public override getMfaFactors(): Promise<{ multiFactors: MfaFactor[] }> {
-    throw new Error("getMfaFactors is not implemented in RNSignerClient");
-  }
+  public override getMfaFactors = async (): Promise<{
path: account-kit/rn-signer/src/client.ts
user: login: moldy530
body: oh yea I just went to check that too. that seems unexpected. there's nothing in these definitions that's environment specific from what I can see
created_at: 2025-04-21T16:10:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -244,52 +284,162 @@ export class RNSignerClient extends BaseSignerClient<undefined> {
   /**
    * Retrieves the list of MFA factors configured for the current user.
    *
-   * @throws {Error} This method is not implemented in RNSignerClient
+   * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to an array of configured MFA factors
+   * @throws {Error} If no user is authenticated
    */
-  public override getMfaFactors(): Promise<{ multiFactors: MfaFactor[] }> {
-    throw new Error("getMfaFactors is not implemented in RNSignerClient");
-  }
+  public override getMfaFactors = async (): Promise<{
path: account-kit/rn-signer/src/client.ts
user: login: blakecduncan
body: Yeah that is a good point, I didn't think about that.  When we first got started with MFA I was only implementing it for the web.  I don't think it ever clicked for me that all these methods can just live in the base client.

I can update that as part of this PR
created_at: 2025-04-21T16:19:52Z
author_association: COLLABORATOR
diff_hunk: @@ -244,52 +284,162 @@ export class RNSignerClient extends BaseSignerClient<undefined> {
   /**
    * Retrieves the list of MFA factors configured for the current user.
    *
-   * @throws {Error} This method is not implemented in RNSignerClient
+   * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to an array of configured MFA factors
+   * @throws {Error} If no user is authenticated
    */
-  public override getMfaFactors(): Promise<{ multiFactors: MfaFactor[] }> {
-    throw new Error("getMfaFactors is not implemented in RNSignerClient");
-  }
+  public override getMfaFactors = async (): Promise<{
path: account-kit/rn-signer/src/client.ts
user: login: jakehobbs
body: sounds good, thanks!
created_at: 2025-04-21T16:22:25Z
author_association: COLLABORATOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-16T13:54:28Z
body: [vc]: #RtL3XcEtikXWQeeRNGE7S6UQOshJgH8z9piypmpQ7VE=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJhYS1zZGstc2l0ZSIsInJvb3REaXJlY3RvcnkiOiJzaXRlIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2FsY2hlbXktZG90LWNvbS9hYS1zZGstc2l0ZS8yYldzWkxuakhaSDM2eHdpQllLa3VabjNyV1Q3IiwicHJldmlld1VybCI6ImFhLXNkay1zaXRlLWdpdC1ibGFrZS1ybi1zaWduZXItbWZhLW1ldGhvZHMtYWxjaGVteS1kb3QtY29tLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiYWEtc2RrLXNpdGUtZ2l0LWJsYWtlLXJuLXNpZ25lci1tZmEtbWV0aG9kcy1hbGNoZW15LWRvdC1jb20udmVyY2VsLmFwcCJ9fSx7Im5hbWUiOiJhYS1zZGstdWktZGVtbyIsInJvb3REaXJlY3RvcnkiOiJleGFtcGxlcy91aS1kZW1vIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2FsY2hlbXktZG90LWNvbS9hYS1zZGstdWktZGVtby84S3JHVm1FbUJiNVU2MmQ0RmRKQWRhNVFqMjFZIiwicHJldmlld1VybCI6ImFhLXNkay11aS1kZW1vLWdpdC1ibGFrZS1ybi1zaWduZXItbWZhLW1ldGhvZHMtYWxjaGVteS1kb3QtY29tLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiYWEtc2RrLXVpLWRlbW8tZ2l0LWJsYWtlLXJuLXNpZ25lci1tZmEtbWV0aG9kcy1hbGNoZW15LWRvdC1jb20udmVyY2VsLmFwcCJ9fV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **aa-sdk-site** | ✅ Ready ([Inspect](https://vercel.com/alchemy-dot-com/aa-sdk-site/2bWsZLnjHZH36xwiBYKkuZn3rWT7)) | [Visit Preview](https://aa-sdk-site-git-blake-rn-signer-mfa-methods-alchemy-dot-com.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/aa-sdk-site-git-blake-rn-signer-mfa-methods-alchemy-dot-com.vercel.app?via=pr-comment-feedback-link) | Apr 21, 2025 9:52pm |
| **aa-sdk-ui-demo** | ✅ Ready ([Inspect](https://vercel.com/alchemy-dot-com/aa-sdk-ui-demo/8KrGVmEmBb5U62d4FdJAda5Qj21Y)) | [Visit Preview](https://aa-sdk-ui-demo-git-blake-rn-signer-mfa-methods-alchemy-dot-com.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/aa-sdk-ui-demo-git-blake-rn-signer-mfa-methods-alchemy-dot-com.vercel.app?via=pr-comment-feedback-link) | Apr 21, 2025 9:52pm |



author_association: NONE
user: login: graphite-app[bot]
created_at: 2025-04-16T13:54:34Z
body: ## How to use the Graphite Merge Queue

Add the label <kbd>graphite-merge-queue</kbd> to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using [this link](https://app.graphite.dev/invite/github/alchemyplatform?ref=merge-queue-instructions-comment&prId=6107642428).

<h2></h2>
<sub>

An organization admin has enabled the [Graphite Merge Queue](https://graphite.dev/docs/graphite-merge-queue) in this repository.

</sub>

<sub>

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

</sub>
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-16T13:54:46Z
body: ## 🌿 Documentation Preview

| Name | Status | Preview | Updated (UTC) |
| :--- | :------ | :------ | :------ |
| **Alchemy Docs** | ✅ Ready | [🔗 Visit Preview](https://alchemy-preview-c5e32937-49e9-47ac-93d7-672d18aca1ab.docs.buildwithfern.com) | Apr 21, 2025, 9:39 PM |

>
author_association: CONTRIBUTOR