diff --git a/extension/package.json b/extension/package.json index 0b1abc4d..85f9ed2b 100644 --- a/extension/package.json +++ b/extension/package.json @@ -47,6 +47,7 @@ "fp-ts": "^2.12.1", "io-ts": "^2.2.16", "lodash-es": "^4.17.21", + "mcl-wasm": "^1.0.3", "phosphor-react": "^1.4.0", "qrcode.react": "^3.1.0", "react": "^17.0.2", diff --git a/extension/source/Home/Wallet/Wallets/Recovery/RecoverWalletModal.tsx b/extension/source/Home/Wallet/Wallets/Recovery/RecoverWalletModal.tsx new file mode 100644 index 00000000..9c2de93e --- /dev/null +++ b/extension/source/Home/Wallet/Wallets/Recovery/RecoverWalletModal.tsx @@ -0,0 +1,108 @@ +import { FunctionComponent, useState } from 'react'; +import Modal from 'react-modal'; +import Button from '../../../../components/Button'; +import Range from '../../../../helpers/Range'; +import StepOneInfo from './StepOneInfo'; +import StepThreeRecover from './StepThreeRecover'; +import StepTwoWalletCreation from './StepTwoWalletCreation'; + +const WorkflowNumbers: FunctionComponent<{ + max: number; + current: number; +}> = ({ max, current }) => { + return ( +
+ {Range(max).map((i) => ( +
+ {i + 1} +
+ ))} +
+ ); +}; + +const RecoverWalletModal = () => { + const [modalIsOpen, setIsOpen] = useState(false); + const [pageIndex, setPageIndex] = useState(0); + const [walletPrivateKey, setWalletPrivateKey] = useState(''); + const [walletAddress, setWalletAddress] = useState(''); + + const onRecoverComplete = () => { + setWalletPrivateKey(''); + setWalletAddress(''); + setIsOpen(false); + }; + + return ( +
+ + setIsOpen(false)} + style={{ + content: { + width: '35rem', + margin: 'auto', + fontFamily: 'montserrat', + padding: 0, + border: 'none', + height: '25rem', + }, + overlay: { + backgroundColor: 'rgba(0, 0, 0, 0.5)', + }, + }} + > +
+ +
+ { + [ + { + setPageIndex(1); + }} + />, + { + setPageIndex(0); + }} + onComplete={() => { + setPageIndex(2); + }} + />, + { + setPageIndex(1); + }} + onComplete={() => onRecoverComplete()} + />, + ][pageIndex] + } +
+
+
+
+ ); +}; + +export default RecoverWalletModal; diff --git a/extension/source/Home/Wallet/Wallets/Recovery/StepOneInfo.tsx b/extension/source/Home/Wallet/Wallets/Recovery/StepOneInfo.tsx new file mode 100644 index 00000000..e8eb51cc --- /dev/null +++ b/extension/source/Home/Wallet/Wallets/Recovery/StepOneInfo.tsx @@ -0,0 +1,38 @@ +import { CaretRight } from 'phosphor-react'; +import { FunctionComponent } from 'react'; +import Button from '../../../../components/Button'; + +const StepOneInfo: FunctionComponent<{ + onComplete: () => void; +}> = ({ onComplete }) => { + return ( +
+
+
Recover existing wallet in Quill
+
+ You can recover existing instant BLS wallets into Quill. This is a + simple 2 step process which requires you to copy-paste two values from + Quill to the instant wallet and then again from instant wallet to + Quill +
+
+
+ Do not close this modal until you have completed all the steps that + follow, else you will lose access to your original keys! +
+
+ +
+ +
+
+ ); +}; + +export default StepOneInfo; diff --git a/extension/source/Home/Wallet/Wallets/Recovery/StepThreeRecover.tsx b/extension/source/Home/Wallet/Wallets/Recovery/StepThreeRecover.tsx new file mode 100644 index 00000000..93ba5454 --- /dev/null +++ b/extension/source/Home/Wallet/Wallets/Recovery/StepThreeRecover.tsx @@ -0,0 +1,101 @@ +import { ethers } from 'ethers'; +import { Download, CaretLeft } from 'phosphor-react'; +import { FunctionComponent, useState } from 'react'; +import Button from '../../../../components/Button'; +import { useQuill } from '../../../../QuillContext'; + +const StepThreeRecover: FunctionComponent<{ + onBack: () => void; + onComplete: () => void; + walletPrivateKey: string; +}> = ({ onBack, onComplete, walletPrivateKey }) => { + const { rpc } = useQuill(); + + const [salt, setSalt] = useState(''); + const [instantWalletAddress, setInstantWalletAddress] = useState(''); + + const handleRecover = async () => { + await rpc.addRecoveryWallet( + instantWalletAddress, + ethers.utils.formatBytes32String(salt), + walletPrivateKey, + ); + onComplete(); + }; + + return ( +
+
+
Almost There
+
+ Your instant wallet recovery is in process. Once finished the same + wallet will be visible inside Quill. +
+
+ Copy back the instant wallet address and salt entered +
+
+ +
+ { + setInstantWalletAddress(e.target.value); + }} + /> + + { + setSalt(e.target.value); + }} + /> +
+
+ +
+ + + +
+
+ ); +}; + +export default StepThreeRecover; diff --git a/extension/source/Home/Wallet/Wallets/Recovery/StepTwoWalletCreation.tsx b/extension/source/Home/Wallet/Wallets/Recovery/StepTwoWalletCreation.tsx new file mode 100644 index 00000000..d91d5134 --- /dev/null +++ b/extension/source/Home/Wallet/Wallets/Recovery/StepTwoWalletCreation.tsx @@ -0,0 +1,93 @@ +import { CaretRight, CaretLeft, CircleNotch, CopySimple } from 'phosphor-react'; +import { FunctionComponent, useEffect, useState } from 'react'; +import Button from '../../../../components/Button'; +import { useQuill } from '../../../../QuillContext'; + +const StepTwoWalletCreation: FunctionComponent<{ + onBack: () => void; + onComplete: () => void; + setWalletPkToParent: (address: string) => void; + setWalletAddressToParent: (address: string) => void; + walletAddress: string; +}> = ({ + onBack, + onComplete, + setWalletPkToParent, + walletAddress, + setWalletAddressToParent, +}) => { + const [loading, setLoading] = useState(!walletAddress); + + const { rpc } = useQuill(); + + useEffect(() => { + const createWallet = async () => { + setLoading(true); + const { address, privateKey } = await rpc.createTempAccount(); + setWalletAddressToParent(address); + setWalletPkToParent(privateKey); + setLoading(false); + }; + + if (!walletAddress) { + createWallet(); + } + }, [walletAddress, rpc, setWalletPkToParent, setWalletAddressToParent]); + + return ( +
+
+
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)} + > + +
+
+ )} +
+
+ +
+ + + +
+
+ ); +}; + +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
- +
+ + +
{!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"