Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/recovery tool #5

Merged
merged 6 commits into from Sep 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion env.d.ts
Expand Up @@ -15,4 +15,4 @@ declare global {
interface Window {
twq: (...args: unknown[]) => void;
}
}
}
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -7,6 +7,7 @@
},
"dependencies": {
"@iconicicons/react": "^1.5.1",
"arweave-mnemonic-keys": "^0.0.9",
"framer-motion": "^10.8.5",
"next": "13.2.4",
"react": "18.2.0",
Expand Down
5 changes: 5 additions & 0 deletions src/components/Button.tsx
@@ -1,5 +1,6 @@
import { Space_Grotesk } from "next/font/google";
import styled from "styled-components";
import Loading from "./Loading";

const spacegrotesk = Space_Grotesk({
subsets: ["latin"],
Expand Down Expand Up @@ -49,6 +50,10 @@ const Button = styled.a<{
width: 1em;
height: 1em;
}

${Loading} {
font-size: 1.1em;
}
`;

export default Button;
1 change: 1 addition & 0 deletions src/components/Footer.tsx
Expand Up @@ -48,6 +48,7 @@ export default function Footer() {
<a href="/support" target="_blank" rel="noopener noreferrer">
Chat
</a>
<Link href="/recover">Recovery</Link>
{/*<Link href="/faq">FAQ</Link>*/}
</LinkColumn>
<LinkColumn>
Expand Down
51 changes: 51 additions & 0 deletions src/components/Loading.tsx
@@ -0,0 +1,51 @@
import styled, { keyframes } from "styled-components";
import { HTMLProps } from "react";

const Loading = styled((props: HTMLProps<SVGElement>) => (
<SvgWrapper
viewBox="0 0 48 48"
xmlns="http://www.w3.org/2000/svg"
{...(props as any)}
>
<circle
cx="24"
cy="24"
fill="none"
r="20"
strokeDasharray="80"
strokeLinecap="round"
stroke="currentColor"
strokeWidth="3"
></circle>
<circle
cx="24"
cy="24"
fill="none"
opacity="0.3"
r="20"
strokeLinecap="round"
stroke="currentColor"
strokeWidth="3"
></circle>
</SvgWrapper>
))``;

const rotate = keyframes`
from {
transform: rotate(0deg);
}

to {
transform: rotate(360deg);
}
`;

const SvgWrapper = styled.svg`
color: currentColor;
font-size: 1em;
width: 1em;
height: 1em;
animation: ${rotate} 0.9s linear infinite;
`;

export default Loading;
6 changes: 5 additions & 1 deletion src/components/landing/Features.tsx
Expand Up @@ -60,7 +60,11 @@ export default function Features() {
</FeatureParagraph>
<Spacer y={2} />
<Buttons>
<Button href="https://docs.arconnect.io/developer-tooling/arconnect-devtools" target="_blank" rel="noopener noreferer">
<Button
href="https://docs.arconnect.io/developer-tooling/arconnect-devtools"
target="_blank"
rel="noopener noreferer"
>
Tooling
<ArrowUpRightIcon />
</Button>
Expand Down
142 changes: 142 additions & 0 deletions src/pages/recover.tsx
@@ -0,0 +1,142 @@
import { Description, ParagraphTitle, Title } from "~/components/content/Text";
import { getKeyFromMnemonic } from "arweave-mnemonic-keys";
import Background from "~/components/landing/Background";
import Section from "~/components/content/Section";
import { WalletIcon } from "@iconicicons/react";
import Loading from "~/components/Loading";
import { Manrope } from "next/font/google";
import Button from "~/components/Button";
import Spacer from "~/components/Spacer";
import Footer from "~/components/Footer";
import styled from "styled-components";
import Head from "~/components/Head";
import Nav from "~/components/Nav";
import { useState } from "react";
import { downloadFile } from "~/utils/file";

const manrope = Manrope({ subsets: ["latin"] });

export default function Recover() {
const [loading, setLoading] = useState(false);
const [mnemonic, setMnemonic] = useState<string>();

async function recover() {
if (!mnemonic) return;
setLoading(true);

try {
const words = mnemonic.replace(/\n/g, "").split(" ");
const key = await getKeyFromMnemonic(words.join(" ") + "\n");

downloadFile(
JSON.stringify(key, null, 2),
"application/json",
"private-key.json"
);
setMnemonic(undefined);
} catch {}

setLoading(false);
}

return (
<>
<Head title="Recover corrupt seedphrase - ArConnect Arweave Wallet" />
<Nav />
<Main>
<Section extraSpace>
<Title>Recover wallet</Title>
<Spacer y={1} />
<Description>
Older versions of ArConnect didn't verify that the input 12 word
seedphrase did not include any extra line breaks. Unfortunately, some
applications (like Apple Notes), store text content such as
seedphrases with an extra line break in the end. In older ArConnect
versions, this could have led to loading a corrupted wallet with a
different address. These wallets are still useable, but cannot be
loaded with the original seedphrase, without adding the extra
linebreak at the end.
</Description>
<Spacer y={1.5} />
<ParagraphTitle>Our advice</ParagraphTitle>
<Spacer y={0.85} />
<Description>
Use the following tool to load the corrupted wallet from the
original seedphrase and download the generated JSON file containing
the private key with your funds. Load the private key into
ArConnect, by adding a new wallet in the settings or during setup,
then{" "}
<i>
<b>transfer your funds to a freshly generated wallet</b>
</i>
.<br />
This will allow you to use a newly generated, correct seedphrase.
Alternatively, you can still access your old wallet with the
downloaded keyfile.
</Description>
<Spacer y={1.5} />
<ParagraphTitle>What this tool is not</ParagraphTitle>
<Spacer y={0.85} />
<Description>
This tool does not exist to recover forgotten or lost private
keys/seedphrases. Our team does not have access to your wallet, so
we cannot help you to recover that.
</Description>
</Section>
<Section>
<SeedArea
placeholder="Enter seedphrase..."
disabled={loading}
value={mnemonic}
onChange={(e) => setMnemonic(e.target.value)}
></SeedArea>
<Spacer y={1.25} />
<Button onClick={recover}>
Recover
{(loading && <Loading />) || <WalletIcon />}
</Button>
</Section>
<Background />
</Main>
<Footer />
</>
);
}

const Main = styled.main`
position: relative;
`;

const SeedArea = styled.textarea`
background-color: transparent;
outline: none;
border: 1.75px solid rgb(${(props) => props.theme.secondaryText}, 0.2);
color: rgb(${(props) => props.theme.secondaryText});
border-radius: 18px;
padding: 0.7rem 1.1rem;
transition: all 0.17s ease-in-out;
${manrope.style}
font-size: .95rem;
font-weight: 550;
line-height: 1.55em;
resize: none;
width: 500px;
height: 160px;

@media screen and (max-width: 720px) {
width: calc(100% - 1.1rem * 2);
}

&::placeholder {
color: rgb(${(props) => props.theme.secondaryText}, 0.2);
}

&:focus {
border-color: rgb(${(props) => props.theme.accent});
}

&:disabled {
cursor: not-allowed;
opacity: 0.7;
}
`;
26 changes: 26 additions & 0 deletions src/utils/file.ts
@@ -0,0 +1,26 @@
/**
* Download a file for the user
*
* @param content Content of the file
* @param contentType File content-type
* @param fileName Name of the file (with the extension)
*/
export function downloadFile(
content: string,
contentType: string,
fileName: string
) {
// create element that downloads the virtual file
const el = document.createElement("a");

el.setAttribute(
"href",
`data:${contentType};charset=utf-8,${encodeURIComponent(content)}`
);
el.setAttribute("download", fileName);
el.style.display = "none";

document.body.appendChild(el);
el.click();
document.body.removeChild(el);
}