-
Notifications
You must be signed in to change notification settings - Fork 158
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Building-Apps / Wallet: add Recovery SEP-30 instructions (#261)
* add ts recovery page * add kotlin examples * trigger preview rebuild * use two identities
- Loading branch information
Showing
1 changed file
with
315 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |