๐Ÿ”Handling recovery

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

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 handleRecover(_ sender: UIButton!) {

    // Get the encrypted user backup share from your API.
    let request = HttpRequest<CipherTextResult, [String : String]>(
      url: self.yourApiUrl + "users/[userId]/user-backup-share?backupMethod=ICLOUD",
      method: "GET",
      body: [:],
      headers: ["Content-Type": "application/json"]
    )

    // Send the request.
    request.send() { (result: Result<Any>) in
      guard result.error == nil else {
        // Handle errors retrieving the backup share.
        return
      }

      // Get the existing backup share from the response.
      let response = result.data as! Dictionary<String, String>
      let cipherText = response["cipherText"]!

      // Provide the user backup share and the backup method to recover.
      self.portal?.recoverWallet(
        cipherText: cipherText,
        method: BackupMethods.iCloud.rawValue
      ) { (result: Result<String>) -> Void in
        // โŒ Handle recover errors.
        guard result.error == nil else {
          guard let mpcError = result.error as? MpcError else {
            // Handle unexpected error types here. Re-run recover.
            return
          }
  
          default:
            // Handle any other errors recovering the wallet. Re-run recover.
            return
          }
        }
      }  progress: { status in
        print("Recover Status: ", status)
      }
    }
  }

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