Recover a wallet

This guide will walk you through how to use your users' backups to recover their wallet.

With Portal-managed backups

When using Portal-managed backups, you can simply call the portal.recoverWallet() function passing an empty string for the cipherText parameter. Portal will then fetch the cipherText from our backend and complete the recovery process.

// With Password
guard let enteredPassword = await requestPasswordFromUser() else {
    return
}
portal.setPassword(enteredPassword)
await portal.recoverWallet(.Password)

// With Google Drive backup
await portal.recoverWallet(.GoogleDrive)

// With Passkeys
await portal.recoverWallet(.Passkey)

WARNING: To recover a wallet with the Portal SDK, your device must be configured to use passcode authentication. Please note that if you disable your passcode authentication after executing the recover function, you will need to run the recover function again to continue using your Portal wallet.

With Backups from your server

Using portal.recoverWallet() allows your users to recover their wallets in the event that their device is lost, broken, or stolen.

The MPC recovery process uses both the user backup shares and custodian backup shares (read about backups in the previous section before reading this) to create a new set of signing shares.

Recovery creates a new set of signing shares, and stores them on the user's device. Before we proceed, please be sure to review our recommendation for your database schema.

User Backup Share Recovery

At this point, a user will have already run portal.backupWallet() and you will have stored their encrypted user backup share in your database. You now need to retrieve this user backup share in order to provide it to portal.recoverWallet(). Behind the scenes our SDK retrieves the encryption key from the user's cloud storage provider and decrypts the encrypted user backup share to be used in the recovery process.

In order to support the user backup share portion of recovery, two dependencies must be met:

  1. You must retrieve the correct user backup share by backup method from your API.

  2. Your app must initiate the recovery process by passing in that encrypted user backup share.

Retrieving the user backup share

Create an API endpoint to get the user backup share, so that the user can start the recovery process.

server.ts
/*
 * This endpoint can be defined by you however you want
 */
app.get('/users/:userId/user-backup-share', async (req, res) => {
  const backupMethod = req.query.backupMethod
  validateBackupMethod(backupMethod) // One of: "GDRIVE", "ICLOUD", "PASSWORD", "PASSKEY"

  const userId = req.params['userId']
  const user = await db.user.findById(userId)

  // Attempt to find the user backup share for the specified backup method.
  const userBackupShare = await db.userBackupShares.find({
    where: {
      backupMethod,
      userId: user.id,
    },
  )

  // If not found, return a 404.
  if (!userBackupShare) {
    res.status(404).json({ message: "Unable to find user backup share" })
    return
  }

  // Return the cipherText of the user backup share if it was found.
  res.status(200).json({ cipherText: userBackupShare.cipherText })
})

Call portal.recoverWallet() with the user backup share

So now you have the user backup share from your API. The next step is to call portal.recoverWallet() with it. Here's an example of how that might look in your Swift code:

Ensure you have created and connected the UI element in the storyboard to the action.

import PortalSwift

struct CipherTextResult: Codable {
  var cipherText: String
}

class ViewController: UIViewController {
  public var portal: Portal?
  public var yourApiUrl: String = "https://YOUR_API_URL.com"
  
  @IBAction func handlePasswordRecover(_: UIButton!) {
    Task {
      do {
        guard let portal = self.portal else {
          throw PortalExampleAppError.portalNotInitialized()
        }

        // Obtain the user's password.
        guard let enteredPassword = await requestPassword(), !enteredPassword.isEmpty else {
          return
        }

        // Set the user's password.
        try portal.setPassword(enteredPassword)

        // Run password recover.
        try await recover(String(user.exchangeUserId), withBackupMethod: .Password)
      } catch {
        // Handle any errors during the recovery process.
      }
    }
  }
  
  public func recover(_ userId: String, withBackupMethod: BackupMethods) async throws -> void {
    guard let portal else {
      throw PortalExampleAppError.portalNotInitialized()
    }

    guard let config else {
      throw PortalExampleAppError.configurationNotSet()
    }

    // Obtain your API's URL for retrieving the encrypted user backup share.
    guard let url = URL(string: "\(yourApiUrl)/users/\(userId)/encrypted-user-backup-shares?backupMethod=\(withBackupMethod.rawValue)") else {
      throw URLError(.badURL)
    }

    // Retrieve the encrypted user backup share on your API.
    let yourApiResponse = try await requests.get(url)
    let decodedResponse = try decoder.decode(CipherTextResult.self, from: yourApiResponse)
    let encryptedUserBackupShare = decodedResponse.encryptedUserBackupShare

    // Run recover.
    try await portal.recoverWallet(withBackupMethod, withCipherText: encryptedUserBackupShare) { status in
      // (Optional) Create a progress indicator here in the progress callback.
    }
    
    // ✅ The user has now recovered with their password successfully!
  }
}

WARNING: To recover a wallet with the Portal SDK, your device must be configured to use passcode authentication. Please note that if you disable your passcode authentication after executing the recover function, you will need to run the recover function again to continue using your Portal wallet.

Status Flow

case readingShare = "Reading share"
case decryptingShare = "Decrypting share"
case parsingShare = "Parsing share" 
case recoveringSigningShare = "Recovering signing share"
case generatingShare = "Generating share"
case parsingShare = "Parsing share" 
case storingShare = "Storing share"
case done = "Done"

Custodian Backup Share Recovery

Recovering your custodian backup share is done after you call portal.recoverWallet(). Portal will attempt to retrieve all of the custodian backup shares for the user via the /backup/fetch webhook.

To support custodian backup share recovery, two dependencies must be met:

  1. Your webhook is configured in the Portal Admin Dashboard.

  2. Your server must support the POST [webhookBaseURL]/backup/fetch webhook endpoint to provide Portal the existing custodian backup shares for the client.

Implementing the /backup/fetch Webhook

Portal will request the existing custodian backup shares with a POST request to [webhookBaseURL]/backup/fetch.

The request body of this POST request will contain one field:

  • clientId - The Portal Id of the user.

app.post('/webhook/backup/fetch', async (req: Request, res: Response) => {
  const { clientId } = req.body
  
  // Obtain all custodian backup shares for the client.
  const custodianBackupShares = await db.custodianBackupShares.findMany({
    where: {
      clientId,
    },
  })
  
  // Ensure to only return back the JSON stringified shares you received from the /backup webhook endpoint.
  const rawShares = custodianBackupShares.map((custodianBackupShare) => {
    return custodianBackupShare.share
  })
  
  res.status(200).send({ backupShares: rawShares })
})

Once we retrieve your custodian backup shares for the client, we will do the heavy lifting to figure out which backup method you're using and select the appropriate custodian backup share to use as a result. So it's as simple as that, just send us all of the custodian backup shares for the client and we'll take care of the rest! 💪

Next steps

Amazing! Your users now have multiple backups and can easily recover their wallet as a result. Next let's dive into handling sessions across multiple devices for your users.

Last updated