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.

Copy

// With Password backup
val backupConfigs = BackupConfigs(PasswordStorageConfig(password = PASSWORD))
portal.recoverWallet(BackupMethods.password, backupConfigs) { status ->
    // (Optional) Get status updates on the recovery operation
}

// With GDrive backup
portal.recoverWallet(BackupMethods.Gdrive) { status ->
    // (Optional) Get status updates on the recovery operation
}

// With PasskeyBackups
await portal.recoverWallet(BackupMethods.Passkey) { status ->
    // (Optional) Get status updates on the recovery operation
}

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 Kotlin code:

// Imports...

class MainActivity : AppCompatActivity() {
  lateinit var portal: Portal
  lateinit var recoverButton: Button

  // Your API instance.
  private val exchangeApi: Api = Api()

  // The user from your API instance.
  lateinit var user: User

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    recoverButton = findViewById(R.id.recoverButton)
    recoverButton.setOnClickListener { handleRecover() }
  }

  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    if (portal.backup.drive.auth.onActivityResult != null) {
      portal.backup.drive.auth.onActivityResult!!(requestCode, resultCode, data)
    }
  }

  private fun handleRecover() {
    CoroutineScope(Dispatchers.IO).launch {
      try {
        // Retrieve the existing encrypted client backup share from your API.
        val cipherText = async { exchangeApi.getCipherText(user.id, "GDRIVE") }

        // Set a new signing share by running recover.
        portal.recoverWallet(cipherText.await()) { status ->
          // Do something with the status, such as update a progress bar
          // or log the progress
          Log.println(Log.INFO, "[PORTAL]", "Recover status: ${status.status} is done: ${status.done}")
        } 
      } catch (err: Error) {
        // ❌ Handle any errors recovering the wallet. Re-run recover.
      }
    }
  }
}

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

ReadingShare("Reading share")
DecryptingShare("Decrypting share")
RecoveringSigningShare("Recovering signing share")
GeneratingShare("Generating share")
ParsingShare("Parsing share")
StoringShare("Storing share")
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 })
})

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