Skip to content

Commit

Permalink
Building-Apps / Wallet: add Recovery SEP-30 instructions (#261)
Browse files Browse the repository at this point in the history
* add ts recovery page

* add kotlin examples

* trigger preview rebuild

* use two identities
  • Loading branch information
acharb authored Oct 31, 2023
1 parent 889c931 commit 3146713
Showing 1 changed file with 315 additions and 0 deletions.
315 changes: 315 additions & 0 deletions docs/building-apps/wallet/sep30.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
---
title: Recovery
sidebar_position: 60
---

import Header from "./component/header.mdx";
import { WalletCodeExample as CodeExample } from "@site/src/components/WalletCodeExample";

<Header WIPLangs={["dart"]} />

The [Sep-30] standard defines the standard way for an individual (e.g., a user or wallet) to regain access to their Stellar account after losing its private key without providing any third party control of the account. During this flow the wallet communicates with one or more recovery signer servers to register the wallet for a later recovery if it's needed.

## Create Recoverable Account

First, let's create an account key, a device key, and a recovery key that will be attached to the account.

<CodeExample>

```typescript
const accountKp = wallet.stellar().account().createKeypair();
const deviceKp = wallet.stellar().account().createKeypair();
const recoveryKp = wallet.stellar().account().createKeypair();
```

```kotlin
val accountKp = wallet.stellar().account().createKeyPair()
val deviceKp = wallet.stellar().account().createKeyPair()
val recoveryKp = wallet.stellar().account().createKeyPair()
```

</CodeExample>

The `accountKp` is the wallet's main account. The `deviceKp` we will be adding to the wallet as a signer so a device (eg. a mobile device a wallet is hosted on) can take control of the account. And the `recoveryKp` will be used to identify the key with the recovery servers.

Next, let's identify the recovery servers and create our recovery object:

<CodeExample>

```typescript
const server1Key = "server1";
const server1 = {
endpoint: "recovery-example.com",
authEndpoint: "auth-example.com",
homeDomain: "test-domain",
};

const server2Key = "server2";
const server2 = {
endpoint: "recovery-example2.com",
authEndpoint: "auth-example2.com",
homeDomain: "test-domain2",
};

const recovery = wallet.recovery({
servers: { [server1Key]: server1, [server2Key]: server2 },
});
```

```kotlin
val first = RecoveryServerKey("first")
val second = RecoveryServerKey("second")
val firstServer = RecoveryServer("recovery.example.com", "auth.example.com", "example.com")
val secondServer = RecoveryServer("recovery2.example.com", "auth2.example.com", "example.com")
val servers = mapOf(first to firstServer, second to secondServer)
val recovery = wallet.recovery(servers)
```

</CodeExample>

Next, we need to define SEP-30 identities. In this example we are going to create an identity for both servers.

<CodeExample>

```typescript
const identity1 = {
role: RecoveryRole.OWNER,
authMethods: [
{
type: RecoveryType.STELLAR_ADDRESS,
value: recoveryKp.publicKey,
},
],
};

const identity2 = {
role: RecoveryRole.OWNER,
authMethods: [
{
type: RecoveryType.STELLAR_ADDRESS,
value: recoveryKp.publicKey,
},
{
type: RecoveryType.EMAIL,
value: "my-email@example.com",
},
],
};
```

```kotlin
val identities =
listOf(
RecoveryAccountIdentity(
RecoveryRole.OWNER,
listOf(RecoveryAccountAuthMethod(RecoveryType.STELLAR_ADDRESS, recoveryKp.address))
)
)
```

</CodeExample>

Here, stellar key will be used as a recovery method. Other recovery servers may support email or phone as a recovery methods.

You can read more about SEP-30 identities [here](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0030.md#common-request-fields)

Next, let's create a recoverable account:

<CodeExample>

```typescript
const config = {
accountAddress: accountKp,
deviceAddress: deviceKp,
accountThreshold: { low: 10, medium: 10, high: 10 },
accountIdentity: { [server1Key]: [identity1], [server2Key]: [identity2] },
signerWeight: { device: 10, recoveryServer: 5 },
};
const recoverableWallet = await recovery.createRecoverableWallet(config);
```

```kotlin
val recoverableWallet =
recovery.createRecoverableWallet(
RecoverableWalletConfig(
accountKp,
deviceKp,
AccountThreshold(10, 10, 10),
mapOf(first to identities, second to identities),
SignerWeight(10, 5)
)
)
```

</CodeExample>

With the given parameters, this function will create a transaction that will:

1. Set `deviceKp` as the primary account key. Please note that the master key belonging to `accountKp` will be locked. `deviceKp` should be used as a primary signer instead.
2. Set all operation thresholds to 10. You can read more about threshold in the [documentation](https://developers.stellar.org/docs/encyclopedia/signatures-multisig#thresholds)
3. Use identities that were defined earlier on both servers. (That means, both server will accept SEP-10 authentication via `recoveryKp` as an auth method)
4. Set device key weight to 10, and recovery server weight to 5. Given these account thresholds, both servers must be used to recover the account, as transaction signed by one will only have weight of 5, which is not sufficient to change account key.

Finally, sign and submit transaction to the network:

<CodeExample>

```typescript
recoverableWallet.transaction.sign(accountKp.keypair);

await stellar.submitTransaction(recoverableWallet.transaction);
```

```kotlin
val tx = recoverableWallet.transaction.sign(accountKp)

wallet.stellar().submitTransaction(tx)
```

</CodeExample>

## Get Account Info

You can fetch account info from one or more servers. To do so, first we need to authenticate with a recovery server using the SEP-10 authentication method:

<CodeExample>

```typescript
const authToken = await recovery
.sep10Auth(server1Key)
.authenticate({ accountKp: recoveryKp });
```

```kotlin
val auth1 = recovery.sep10Auth(first).authenticate(recoveryKp)
```

</CodeExample>

Next, get account info using auth tokens:

<CodeExample>

```typescript
const accountResp = await recovery.getAccountInfo(accountKp, {
[server1Key]: authToken,
});
```

```kotlin
val accountInfo = recovery.getAccountInfo(account, mapOf(first to auth1))

println("Recoverable info: $accountInfo")
```

</CodeExample>

## Recover Wallet

Let's say we've lost our device key and need to recover our wallet.

First, we need to authenticate with both recovery servers:

<CodeExample>

```typescript
const authToken1 = await recovery
.sep10Auth(server1Key)
.authenticate({ accountKp: recoveryKp });

const authToken2 = await recovery
.sep10Auth(server1Key)
.authenticate({ accountKp: recoveryKp });
```

```kotlin
val auth1 = recovery.sep10Auth(first).authenticate(recoveryKp)
val auth2 = recovery.sep10Auth(second).authenticate(recoveryKp)
```

</CodeExample>

We need to know the recovery signer addresses that will be used to sign the transaction. You can get them from either the recoverable wallet object we created earlier (`recoverableWallet.signers`), or via fetching account info from recovery servers.

<CodeExample>

```typescript
const recoverySignerAddress1 = recoverableWallet.signers[0];
const recoverySignerAddress2 = recoverableWallet.signers[1];
```

```kotlin
val recoverySigners = recoverableWallet.signers
```

</CodeExample>

Next, create a new device key and retrieve a signed transaction that replaces the device key:

<CodeExample>

```typescript
const newDeviceKp = accountService.createKeypair();

const serverAuth = {
[server1Key]: {
signerAddress: recoverySignerAddress1,
authToken1,
},
[server2Key]: {
signerAddress: recoverySignerAddress2,
authToken2,
},
};

const recoverTxn = await recovery.replaceDeviceKey(
accountKp,
newDeviceKp,
serverAuth,
);
```

```kotlin
val newKey = wallet.stellar().account().createKeyPair()

val serverAuth = mapOf(
first to RecoveryServerSigning(recoverySigners[0], auth1),
second to RecoveryServerSigning(recoverySigners[1], auth2)
)

val signedReplaceKeyTransaction =
recovery.replaceDeviceKey(
accountKp,
newKey,
serverAuth
)
```

</CodeExample>

Calling this function will create a transaction that locks the previous device key and replaces it with your new key (having the same weight as the old one). Both recovery signers will have signed the transaction.

The lost device key is deduced automatically if not given. A signer will be considered a device key, if one of these conditions matches:

1. It's the only signer that's not in `serverAuth`.
2. All signers in `serverAuth` have the same weight, and the potential signer is the only one with a different weight.

Note that the account created above will match the first criteria. If 2-3 schema were used, then second criteria would match. (In 2-3 schema, 3 serves are used and 2 of them is enough to recover key. This is a recommended approach.)

Note: you can also use more low-level `signWithRecoveryServers` functions to sign arbitrary transaction.

Finally, it's time to submit the transaction:

<CodeExample>

```typescript
await stellar.submitTransaction(recoverTxn);
```

```kotlin
wallet.stellar().submitTransaction(signedReplaceKeyTransaction)
```

</CodeExample>

[sep-30]: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0030.md

0 comments on commit 3146713

Please sign in to comment.