+
+
Recovery Wallet
+
+ This is the wallet address which will be used to recover your instant
+ wallet.
+
+
+
+ Copy the address and paste it in the instant wallet.
+
+
+ {loading ? (
+
+ Generating wallet
+
+
+
+
+ ) : (
+
+
{walletAddress}
+ {/* eslint-disable-next-line */}
+
navigator.clipboard.writeText(walletAddress)}
+ >
+
+
+
+ )}
+
+
+
+
+ onBack()}
+ className="btn-primary h-10 text-[10pt] w-1/3"
+ iconLeft={ }
+ >
+ Back
+
+
+ onComplete()}
+ className="btn-primary h-10 text-[10pt] w-1/3"
+ icon={ }
+ >
+ Continue
+
+
+
+ );
+};
+
+export default StepTwoWalletCreation;
diff --git a/extension/source/Home/Wallet/Wallets/WalletWrapper.tsx b/extension/source/Home/Wallet/Wallets/WalletWrapper.tsx
index 0d1e7e99..34d5c5a0 100644
--- a/extension/source/Home/Wallet/Wallets/WalletWrapper.tsx
+++ b/extension/source/Home/Wallet/Wallets/WalletWrapper.tsx
@@ -3,6 +3,7 @@ import useCell from '../../../cells/useCell';
import Button from '../../../components/Button';
import Loading from '../../../components/Loading';
import { useQuill } from '../../../QuillContext';
+import RecoverWalletModal from './Recovery/RecoverWalletModal';
/* eslint import/no-cycle: "warn" -- TODO (merge-ok) Fix import cycle */
import { WalletSummary } from './WalletSummary';
@@ -25,9 +26,12 @@ export const WalletsWrapper: FunctionComponent = () => {
Wallets
-
- Add
-
+
+
+
+ Add
+
+
{!ethAccounts &&
}
diff --git a/extension/source/background/KeyringController.ts b/extension/source/background/KeyringController.ts
index 0bce8f8a..43cee709 100644
--- a/extension/source/background/KeyringController.ts
+++ b/extension/source/background/KeyringController.ts
@@ -1,7 +1,11 @@
-import { BlsWalletWrapper } from 'bls-wallet-clients';
-import { ethers } from 'ethers';
-import { keccak256 } from 'ethers/lib/utils';
-import generateRandomHex from '../helpers/generateRandomHex';
+import {
+ BlsWalletWrapper,
+ // eslint-disable-next-line camelcase
+ VerificationGateway__factory,
+ Aggregator,
+} from 'bls-wallet-clients';
+import { ethers, BigNumberish } from 'ethers';
+import { solidityPack, keccak256 } from 'ethers/lib/utils';
import QuillStorageCells from '../QuillStorageCells';
import assert from '../helpers/assert';
import { PartialRpcImpl, RpcClient } from '../types/Rpc';
@@ -10,6 +14,8 @@ import { MultiNetworkConfig } from '../MultiNetworkConfig';
import { IReadableCell } from '../cells/ICell';
import mixtureCopy from '../cells/mixtureCopy';
import getNetworkConfig from './getNetworkConfig';
+import randFr from '../helpers/randFr';
+import generateRandomHex from '../helpers/generateRandomHex';
export default class KeyringController {
constructor(
@@ -110,6 +116,19 @@ export default class KeyringController {
return networkData.address;
},
+ createTempAccount: async (_request) => {
+ const pKey = `0x${(await randFr()).serializeToHexStr()}`;
+ const { wallets } = await this.keyring.read();
+
+ assert(
+ wallets.every((w) => w.privateKey !== pKey),
+ () => new Error('Wallet already exists'),
+ );
+
+ const { address, privateKey } = await this.BlsWalletWrapper(pKey);
+ return { address, privateKey };
+ },
+
addAccount: async ({ params: [privateKey = generateRandomHex(256)] }) => {
const { wallets } = await this.keyring.read();
@@ -154,8 +173,53 @@ export default class KeyringController {
await this.keyring.update({ wallets: newWallets });
},
+
+ /**
+ * Recovers an existing BLS wallet and adds the new
+ * recovered wallet to the keyring
+ * @param recoveryWalletAddress Smart contract address
+ * of wallet being recovered
+ * @param recoverySaltHash Salt used to set the recovery
+ * hash on the wallet that is being recovered
+ * @param signerWalletPrivateKey The private key of the wallet that is used
+ * to generate the recovery hash. This wallet will sign the 'recoverWallet'
+ * request.
+ */
+ addRecoveryWallet: async ({
+ params: [recoveryWalletAddress, recoverySaltHash, signerWalletPrivateKey],
+ }) => {
+ const privateKey = await this.recoverWallet(
+ recoveryWalletAddress,
+ recoverySaltHash,
+ signerWalletPrivateKey,
+ );
+
+ // Add new private key to the keyring
+ await this.InternalRpc().addAccount(privateKey);
+ await this.swapContractWalletAddress(privateKey, recoveryWalletAddress);
+ },
});
+ async swapContractWalletAddress(pKey: string, newAddress: string) {
+ const network = await this.network.read();
+ const { wallets } = await this.keyring.read();
+
+ const currentWallet = wallets.find((w) => w.privateKey === pKey);
+ // get all wallets without the current wallet
+ const updatedWallets = wallets.filter((w) => w.privateKey !== pKey);
+
+ if (currentWallet) {
+ const networkDetails = currentWallet.networks[network.networkKey];
+ if (networkDetails) {
+ networkDetails.address = newAddress;
+ }
+ currentWallet.networks[network.networkKey] = networkDetails;
+ // push the updated wallet to all wallets array
+ updatedWallets.push(currentWallet);
+ }
+ await this.keyring.update({ wallets: updatedWallets });
+ }
+
async BlsWalletWrapper(privateKey: string): Promise
{
const netCfg = getNetworkConfig(
await this.network.read(),
@@ -214,4 +278,93 @@ export default class KeyringController {
{},
);
}
+
+ async signWalletAddress(
+ senderAddress: string,
+ signerPrivateKey: string,
+ ): Promise<[BigNumberish, BigNumberish]> {
+ const netCfg = getNetworkConfig(
+ await this.network.read(),
+ this.multiNetworkConfig,
+ );
+
+ const addressMessage = solidityPack(['address'], [senderAddress]);
+ const wallet = await BlsWalletWrapper.connect(
+ signerPrivateKey,
+ netCfg.addresses.verificationGateway,
+ await this.ethersProvider.read(),
+ );
+ return wallet.signMessage(addressMessage);
+ }
+
+ async recoverWallet(
+ recoveryWalletAddress: string,
+ recoverySaltHash: string,
+ signerWalletPrivateKey: string,
+ ): Promise {
+ const network = await this.network.read();
+ const netCfg = getNetworkConfig(network, this.multiNetworkConfig);
+
+ // Create new private key for the wallet we are recovering to.
+ const newPrivateKey = `0x${(await randFr()).serializeToHexStr()}`;
+
+ const addressSignature = await this.signWalletAddress(
+ recoveryWalletAddress,
+ newPrivateKey,
+ );
+
+ // Get instance of the new wallet, so we can get the public key
+ // to pass to the recoverWallet method.
+ const newWalletWrapper = await this.BlsWalletWrapper(newPrivateKey);
+
+ // eslint-disable-next-line camelcase
+ const verificationGatewayContract = VerificationGateway__factory.connect(
+ netCfg.addresses.verificationGateway,
+ await this.ethersProvider.read(),
+ );
+
+ const recoveryWalletHash = await verificationGatewayContract.hashFromWallet(
+ recoveryWalletAddress,
+ );
+
+ const signerWallet = await this.BlsWalletWrapper(signerWalletPrivateKey);
+
+ const nonce = await BlsWalletWrapper.Nonce(
+ signerWallet.PublicKey(),
+ netCfg.addresses.verificationGateway,
+ await this.ethersProvider.read(),
+ );
+
+ // Thought about using this.InternalRpc().eth_sendTransaction() here.
+ // However since we are generating a wallet on the fly and not using
+ // an existing wallet in the keyring I am calling creating a bundle
+ // manually and submitting it to the aggregator.
+ const bundle = signerWallet.sign({
+ nonce,
+ actions: [
+ {
+ ethValue: 0,
+ contractAddress: verificationGatewayContract.address,
+ encodedFunction:
+ verificationGatewayContract.interface.encodeFunctionData(
+ 'recoverWallet',
+ [
+ addressSignature,
+ recoveryWalletHash,
+ recoverySaltHash,
+ newWalletWrapper.PublicKey(),
+ ],
+ ),
+ },
+ ],
+ });
+
+ const { aggregatorUrl } = network;
+ const agg = new Aggregator(aggregatorUrl);
+ const result = await agg.add(bundle);
+
+ assert(!('failures' in result), () => new Error(JSON.stringify(result)));
+
+ return newPrivateKey;
+ }
}
diff --git a/extension/source/background/QuillController.ts b/extension/source/background/QuillController.ts
index 770351de..86af5512 100644
--- a/extension/source/background/QuillController.ts
+++ b/extension/source/background/QuillController.ts
@@ -165,7 +165,9 @@ export default class QuillController {
lookupPrivateKey: this.keyringController.rpc.lookupPrivateKey,
pkHashToAddress: this.keyringController.rpc.pkHashToAddress,
addAccount: this.keyringController.rpc.addAccount,
+ createTempAccount: this.keyringController.rpc.createTempAccount,
removeAccount: this.keyringController.rpc.removeAccount,
+ addRecoveryWallet: this.keyringController.rpc.addRecoveryWallet,
// TransactionsController
createTransaction: this.transactionsController.rpc.createTransaction,
diff --git a/extension/source/helpers/randFr.ts b/extension/source/helpers/randFr.ts
new file mode 100644
index 00000000..49deaa20
--- /dev/null
+++ b/extension/source/helpers/randFr.ts
@@ -0,0 +1,11 @@
+import * as mcl from 'mcl-wasm';
+import { hexlify, randomBytes } from 'ethers/lib/utils';
+
+export default async function randFr(): Promise {
+ await mcl.init(mcl.BN_SNARK1);
+ mcl.setMapToMode(mcl.BN254);
+ const r = hexlify(randomBytes(12));
+ const fr = new mcl.Fr();
+ fr.setHashOf(r);
+ return fr;
+}
diff --git a/extension/source/types/Rpc.ts b/extension/source/types/Rpc.ts
index 922abed9..9b00f463 100644
--- a/extension/source/types/Rpc.ts
+++ b/extension/source/types/Rpc.ts
@@ -145,6 +145,11 @@ export const rpcMap = {
Params: optional(emptyTuple),
Response: io.string,
},
+ createTempAccount: {
+ origin: '',
+ Params: optional(emptyTuple),
+ Response: io.type({ address: io.string, privateKey: io.string }),
+ },
setHDPhrase: {
origin: '',
Params: io.tuple([io.string]),
@@ -165,6 +170,11 @@ export const rpcMap = {
Params: io.tuple([io.string]),
Response: io.void,
},
+ addRecoveryWallet: {
+ origin: '',
+ Params: io.tuple([io.string, io.string, io.string]),
+ Response: io.void,
+ },
// TransactionController
diff --git a/extension/yarn.lock b/extension/yarn.lock
index 523b7f3a..6b165ebb 100644
--- a/extension/yarn.lock
+++ b/extension/yarn.lock
@@ -4896,6 +4896,11 @@ mcl-wasm@^1.0.0:
resolved "https://registry.yarnpkg.com/mcl-wasm/-/mcl-wasm-1.0.2.tgz#2a891f2ca83c158ec453d9522957dcc6e56e28ed"
integrity sha512-rYTsi5HRjfTUbSvaERM0t7FJm9smRFRKSqAGnlUttfDvaJxqlOaEP8rnFMWkR4mVvFQLhclnpyR8lJmE478GGg==
+mcl-wasm@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/mcl-wasm/-/mcl-wasm-1.0.3.tgz#b056c655270c3a5e0473489ceb9e55b3de924998"
+ integrity sha512-L8hexPDw02JEXscEm4pB2rvfAYRc4HIsssxcj+I1AGC4/LYFy9GyrmCgFC+CzxKtxuRQcuBi1RLw74MAzZ5V2Q==
+
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"