Skip to content

Commit

Permalink
Create wallet from bip39 seed words.
Browse files Browse the repository at this point in the history
Signed-off-by: Philipp Hoenisch <[email protected]>
  • Loading branch information
bonomat committed Jul 27, 2021
1 parent 43c69a8 commit 6f1a650
Show file tree
Hide file tree
Showing 11 changed files with 510 additions and 125 deletions.
299 changes: 268 additions & 31 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions extension/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import AddressQr from "./components/AddressQr";
import WalletBalances from "./components/Balances";
import ConfirmLoan from "./components/ConfirmLoan";
import ConfirmSwap from "./components/ConfirmSwap";
import UnlockWallet from "./components/UnlockWallet";
import CreateWallet from "./components/CreateWallet";
import OpenLoans from "./components/OpenLoans";
import UnlockWallet from "./components/UnlockWallet";
import WithdrawAll from "./components/WithdrawAll";
import { Status } from "./models";
import theme from "./theme";
import CreateWallet from "./components/CreateWallet";

const App = () => {
const walletStatusHook = useAsync({ promiseFn: getWalletStatus });
Expand Down
9 changes: 7 additions & 2 deletions extension/src/background-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,14 @@ export async function getWalletStatus(): Promise<WalletStatus> {
return proxy.getWalletStatus();
}

export async function createWallet(password: string): Promise<void> {
export async function createWalletFromBip39(seed_words: string, password: string): Promise<void> {
// @ts-ignore
return proxy.createWallet(password);
return proxy.createWalletFromBip39(seed_words, password);
}

export async function bip39SeedWords(): Promise<string> {
// @ts-ignore
return proxy.bip39SeedWords();
}

export async function unlockWallet(password: string): Promise<void> {
Expand Down
15 changes: 10 additions & 5 deletions extension/src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { browser } from "webextension-polyfill-ts";
import { Direction, Message, MessageKind } from "../messages";
import { LoanDetails, LoanToSign, SwapToSign } from "../models";
import {
createWallet,
bip39SeedWords,
createNewBip39Wallet,
extractLoan,
extractTrade,
getAddress,
Expand Down Expand Up @@ -109,10 +110,6 @@ async function call_wallet<T>(wallet_fn: () => Promise<T>, kind: MessageKind): P
return { kind, direction: Direction.ToPage, payload, error: err };
}

// @ts-ignore
window.createWallet = async (password: string) => {
return createWallet(walletName, password);
};
// @ts-ignore
window.getWalletStatus = async () => {
return walletStatus(walletName);
Expand Down Expand Up @@ -202,6 +199,14 @@ window.repayLoan = async (txid: string): void => {
window.getPastTransactions = async (): Txid[] => {
return getPastTransactions(walletName);
};
// @ts-ignore
window.bip39SeedWords = async (): string => {
return bip39SeedWords();
};
// @ts-ignore
window.createWalletFromBip39 = async (seed_words: string, password: string) => {
return createNewBip39Wallet(walletName, seed_words, password);
};

function updateBadge() {
let count = 0;
Expand Down
170 changes: 123 additions & 47 deletions extension/src/components/CreateWallet.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
import { Button, FormControl, FormErrorMessage, Input, InputGroup, InputRightElement } from "@chakra-ui/react";
import { RepeatIcon } from "@chakra-ui/icons";
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
Button,
Checkbox,
FormControl,
FormErrorMessage,
IconButton,
Input,
InputGroup,
InputRightElement,
Textarea,
} from "@chakra-ui/react";
import Debug from "debug";
import * as React from "react";
import { ChangeEvent, useState } from "react";
import { useAsync } from "react-async";
import { createWallet, unlockWallet } from "../background-proxy";
import { Status } from "../models";
import { bip39SeedWords, createWalletFromBip39 } from "../background-proxy";

Debug.enable("*");
const debug = Debug("unlock-wallet");
Expand All @@ -14,60 +30,120 @@ type CreateWalletProps = {
};

function CreateWallet({ onUnlock }: CreateWalletProps) {
const [show, setShow] = React.useState(false);
const [backedUp, setBackedUp] = useState(false);
const [seedWords, setSeedWords] = useState(
"chase prevent symptom panel promote short tray cigar wonder vanish sustain hurry",
);
const [backedUpSeedWords, setBackedUpSeedWords] = useState("");
const [show, setShow] = useState(false);
const [password, setPassword] = useState("");

const onPasswordChange = (event: ChangeEvent<HTMLInputElement>) => setPassword(event.target.value);
const handleClick = () => setShow(!show);
const toggleShowPassword = () => setShow(!show);

let { run, isPending, isRejected } = useAsync({
let { run: createWallet, isPending: isCreatingWallet, isRejected: createWalletIsRejected } = useAsync({
deferFn: async () => {
await createWallet(password);
await createWalletFromBip39(backedUpSeedWords, password);
onUnlock();
},
onReject: (e) => debug("Failed to create wallet: %s", e),
onReject: (e) => debug("Failed to unlock wallet: %s", e),
});
let { run: newSeedWords, isPending: isGeneratingSeedWords, isRejected: generatingSeedWordsFailed } = useAsync({
deferFn: async () => {
let words = await bip39SeedWords();
setSeedWords(words);
},
onReject: (e) => debug("Failed to unlock wallet: %s", e),
});

return (
<>
<form
onSubmit={async e => {
e.preventDefault();
run();
}}
>
<FormControl id="password" isInvalid={isRejected}>
<InputGroup size="md">
<Input
pr="4.5rem"
type={show ? "text" : "password"}
placeholder="Enter password"
value={password}
onChange={onPasswordChange}
data-cy={"data-cy-create-wallet-password-input"}
<Accordion>
<AccordionItem>
<h2>
<AccordionButton>
<Box flex="1" textAlign="left">
Generate seed words
</Box>
<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb={4}>
<Textarea
placeholder={seedWords}
value={seedWords}
isInvalid={generatingSeedWordsFailed}
onChange={(event) => setSeedWords(event.target.value)}
/>
<IconButton
aria-label="Refresh"
icon={<RepeatIcon />}
isLoading={isGeneratingSeedWords}
onClick={(_) => {
newSeedWords();
}}
/>
<Checkbox isChecked={backedUp} onChange={_ => setBackedUp(!backedUp)}>
I confirm that I have a secure backup of the seed words
</Checkbox>
</AccordionPanel>
</AccordionItem>

<AccordionItem isDisabled={!backedUp}>
<h2>
<AccordionButton>
<Box flex="1" textAlign="left">
Confirm seed words
</Box>

<AccordionIcon />
</AccordionButton>
</h2>
<AccordionPanel pb={4}>
<form
onSubmit={async e => {
e.preventDefault();
await createWallet();
}}
>
<Textarea
placeholder={"Your seed words..."}
value={backedUpSeedWords}
onChange={(event) => setBackedUpSeedWords(event.target.value)}
/>
<InputRightElement width="4.5rem">
<Button
h="1.75rem"
size="sm"
onClick={handleClick}
data-cy={"data-cy-create-wallet-button"}
>
{show ? "Hide" : "Show"}
</Button>
</InputRightElement>
</InputGroup>
<FormErrorMessage>Failed to create wallet. Wrong password?</FormErrorMessage>
</FormControl>
<Button
type="submit"
variant="solid"
isLoading={isPending}
data-cy={"data-cy-create-or-unlock-wallet-button"}
>
{"Create"}
</Button>
</form>
</>
<FormControl id="password" isInvalid={createWalletIsRejected}>
<InputGroup size="md">
<Input
pr="4.5rem"
type={show ? "text" : "password"}
placeholder="Enter password"
value={password}
onChange={onPasswordChange}
data-cy={"data-cy-create-wallet-password-input"}
/>
<InputRightElement width="4.5rem">
<Button
h="1.75rem"
size="sm"
onClick={toggleShowPassword}
>
{show ? "Hide" : "Show"}
</Button>
</InputRightElement>
</InputGroup>
<FormErrorMessage>Failed to unlock wallet. Wrong password?</FormErrorMessage>
</FormControl>
<Button
type="submit"
variant="solid"
isLoading={isCreatingWallet}
data-cy={"data-cy-create-wallet-button"}
>
{"Create"}
</Button>
</form>
</AccordionPanel>
</AccordionItem>
</Accordion>
);
}

Expand Down
21 changes: 14 additions & 7 deletions extension/src/wasmProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,6 @@ export async function getAddress(name: string): Promise<Address> {
return get_address(name);
}

export async function createWallet(name: string, password: string): Promise<void> {
const { create_new_wallet } = await import("./wallet");

debug("createWallet");
return create_new_wallet(name, password);
}

export async function unlockWallet(name: string, password: string): Promise<void> {
const { load_existing_wallet } = await import("./wallet");

Expand Down Expand Up @@ -128,3 +121,17 @@ export async function getPastTransactions(name: string): Promise<Txid[]> {
debug("getPastTransactions");
return get_past_transactions(name);
}

export async function bip39SeedWords(): Promise<string> {
const { bip39_seed_words } = await import("./wallet");

debug("bip39_seed_words");
return bip39_seed_words();
}

export async function createNewBip39Wallet(name: string, seedWords: string, password: string): Promise<string> {
const { create_new_bip39_wallet } = await import("./wallet");

debug("create_new_bip39_wallet");
return create_new_bip39_wallet(name, seedWords, password);
}
2 changes: 2 additions & 0 deletions extension/wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ aes-gcm-siv = { version = "0.9", features = [ "std" ] }
anyhow = "1"
baru = "0.1"
bdk = { version = "0.4", default-features = false }
bip32 = { version = "0.2", features = [ "secp256k1" ] }
bip39 = { version = "1", features = [ "rand" ] }
coin_selection = { path = "../../coin_selection" }
conquer-once = "0.3"
console_error_panic_hook = { version = "0.1.6", optional = true }
Expand Down
27 changes: 23 additions & 4 deletions extension/wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ mod storage;
mod wallet;

use crate::{storage::Storage, wallet::*};
use bip39::{Language, Mnemonic};
use itertools::Itertools;
use reqwest::Url;

// TODO: make this configurable through extension option UI
Expand Down Expand Up @@ -85,13 +87,30 @@ pub fn setup() {
handler.forget();
}

/// Create a new wallet with the given name and password.
/// Generates random seed words with length 12 and language english
#[wasm_bindgen]
pub fn bip39_seed_words() -> Result<JsValue, JsValue> {
let mnemonic = map_err_from_anyhow!(wallet::bip39_seed_words(Language::English, 12))?;
let words = mnemonic.word_iter().join(" ");

Ok(JsValue::from_str(words.as_str()))
}

/// Create a new wallet from the given seed words (mnemonic) with the given name and password.
///
/// Fails if a wallet with this name already exists.
/// Fails if a the seed words are invalid or if the wallet with this name already exists.
/// The created wallet will be automatically loaded.
#[wasm_bindgen]
pub async fn create_new_wallet(name: String, password: String) -> Result<JsValue, JsValue> {
map_err_from_anyhow!(wallet::create_new(name, password, &LOADED_WALLET).await)?;
pub async fn create_new_bip39_wallet(
name: String,
seed_words: String,
password: String,
) -> Result<JsValue, JsValue> {
let mnemonic = map_err_from_anyhow!(Mnemonic::from_str(seed_words.as_str()))?;

map_err_from_anyhow!(
wallet::create_from_bip39(name, mnemonic, password, &LOADED_WALLET).await
)?;

Ok(JsValue::null())
}
Expand Down
Loading

0 comments on commit 6f1a650

Please sign in to comment.