feat: keystore and recovery seed
This commit is contained in:
parent
3040bd5a57
commit
ecbcc08209
@ -1,6 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, defineEmits } from 'vue'
|
import { ref, computed, defineEmits } from 'vue'
|
||||||
import { ButtonCommon, FormCommon, KeystoreDownloadComponent } from '@/components'
|
import {
|
||||||
|
ButtonCommon,
|
||||||
|
FormCommon,
|
||||||
|
KeystoreDownloadComponent,
|
||||||
|
RecoverySeedComponent,
|
||||||
|
} from '@/components'
|
||||||
|
import { generateSeedPhrase18 } from '@/utils/helpers/seedPhrase'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
navigateToOpenWallet: [event: Event]
|
navigateToOpenWallet: [event: Event]
|
||||||
@ -8,6 +14,8 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const step = ref(1)
|
const step = ref(1)
|
||||||
|
const backupMethod = ref<'none' | 'seed' | 'keystore'>('none')
|
||||||
|
const generatedSeed = ref<string[] | null>(null)
|
||||||
|
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
const confirmPassword = ref('')
|
const confirmPassword = ref('')
|
||||||
@ -80,9 +88,27 @@ function downloadKeystoreFile() {
|
|||||||
setTimeout(() => URL.revokeObjectURL(link.href), 2300)
|
setTimeout(() => URL.revokeObjectURL(link.href), 2300)
|
||||||
step.value = 3
|
step.value = 3
|
||||||
}
|
}
|
||||||
|
const handleChooseMethod = (method: 'seed' | 'keystore'): void => {
|
||||||
|
backupMethod.value = method
|
||||||
|
if (method === 'seed') {
|
||||||
|
generatedSeed.value = generateSeedPhrase18()
|
||||||
|
}
|
||||||
|
step.value = 3
|
||||||
|
}
|
||||||
|
const handleNextRecovery = () => {
|
||||||
|
step.value = 4
|
||||||
|
}
|
||||||
|
const handleBackChoose = () => {
|
||||||
|
backupMethod.value = 'none'
|
||||||
|
generatedSeed.value = null
|
||||||
|
step.value = 2
|
||||||
|
}
|
||||||
function handleBack() {
|
function handleBack() {
|
||||||
if (step.value === 2) step.value = 1
|
if (step.value === 2) step.value = 1
|
||||||
else if (step.value === 3) step.value = 2
|
else if (step.value === 3) {
|
||||||
|
backupMethod.value = 'none'
|
||||||
|
step.value = 2
|
||||||
|
} else if (step.value === 4) step.value = 2
|
||||||
}
|
}
|
||||||
function resetAll() {
|
function resetAll() {
|
||||||
password.value = ''
|
password.value = ''
|
||||||
@ -250,9 +276,44 @@ function resetAll() {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="step === 2">
|
<template v-else-if="step === 2">
|
||||||
<KeystoreDownloadComponent @download="downloadKeystoreFile" @back="handleBack" />
|
<div class="choose-backup-method">
|
||||||
|
<h2>Choose your backup method</h2>
|
||||||
|
<div class="choose-btns">
|
||||||
|
<ButtonCommon
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
@click="() => handleChooseMethod('seed')"
|
||||||
|
>
|
||||||
|
Mnemonic Phrase
|
||||||
|
</ButtonCommon>
|
||||||
|
<ButtonCommon
|
||||||
|
type="default"
|
||||||
|
size="large"
|
||||||
|
@click="() => handleChooseMethod('keystore')"
|
||||||
|
>
|
||||||
|
Keystore File
|
||||||
|
</ButtonCommon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="step === 3">
|
<template v-else-if="step === 3">
|
||||||
|
<template v-if="backupMethod === 'seed'">
|
||||||
|
<RecoverySeedComponent
|
||||||
|
:seed-words="generatedSeed"
|
||||||
|
@next="handleNextRecovery"
|
||||||
|
@back="handleBackChoose"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<KeystoreDownloadComponent
|
||||||
|
@download="downloadKeystoreFile"
|
||||||
|
@back="handleBackChoose"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="step === 4">
|
||||||
<div class="well-done-step">
|
<div class="well-done-step">
|
||||||
<h2 class="done-main">You are done!</h2>
|
<h2 class="done-main">You are done!</h2>
|
||||||
<p class="done-desc">
|
<p class="done-desc">
|
||||||
@ -485,6 +546,23 @@ function resetAll() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.choose-backup-method {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 8px;
|
||||||
|
h2 {
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
}
|
||||||
|
.choose-btns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.auth-container {
|
.auth-container {
|
||||||
padding: var(--spacing-md);
|
padding: var(--spacing-md);
|
||||||
|
|||||||
@ -6,16 +6,16 @@ import { validateSeedPhrase18 } from '@/utils/helpers/seedPhrase'
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(
|
(
|
||||||
e: 'import-success',
|
e: 'import-success',
|
||||||
data: { type: 'seed' | 'privatekey'; value: string | string[]; passphrase?: string }
|
data: { type: 'seed' | 'keystore'; value: string | string[]; passphrase?: string }
|
||||||
): void
|
): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const tab = ref<'seedphrase' | 'privatekey'>('seedphrase')
|
const tab = ref<'seedphrase' | 'keystore'>('seedphrase')
|
||||||
const seedWords = ref<string[]>(Array(18).fill(''))
|
const seedWords = ref<string[]>(Array(18).fill(''))
|
||||||
const seedError = ref('')
|
const seedError = ref('')
|
||||||
const passphrase = ref('')
|
const passphrase = ref('')
|
||||||
const privateKey = ref('')
|
const keystore = ref('')
|
||||||
const privateKeyError = ref('')
|
const keystoreError = ref('')
|
||||||
|
|
||||||
const inputBoxFocus = (idx: number) => {
|
const inputBoxFocus = (idx: number) => {
|
||||||
document.getElementById('input-' + idx)?.focus()
|
document.getElementById('input-' + idx)?.focus()
|
||||||
@ -34,12 +34,12 @@ const validateSeed = () => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateKey = () => {
|
const validateKeystore = () => {
|
||||||
if (!privateKey.value.trim()) {
|
if (!keystore.value.trim()) {
|
||||||
privateKeyError.value = 'Please enter your private key.'
|
keystoreError.value = 'Please enter your keystore.'
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
privateKeyError.value = ''
|
keystoreError.value = ''
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,8 +53,8 @@ const handleContinue = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (validateKey()) {
|
if (validateKeystore()) {
|
||||||
emit('import-success', { type: 'privatekey', value: privateKey.value })
|
emit('import-success', { type: 'keystore', value: keystore.value })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,11 +70,8 @@ const handleContinue = () => {
|
|||||||
>
|
>
|
||||||
Import by seed phrase
|
Import by seed phrase
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button :class="['tab-btn', tab === 'keystore' && 'active']" @click="tab = 'keystore'">
|
||||||
:class="['tab-btn', tab === 'privatekey' && 'active']"
|
Import by keystore
|
||||||
@click="tab = 'privatekey'"
|
|
||||||
>
|
|
||||||
Import by private key
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="tab === 'seedphrase'" class="tab-pane">
|
<div v-if="tab === 'seedphrase'" class="tab-pane">
|
||||||
@ -113,12 +110,12 @@ const handleContinue = () => {
|
|||||||
<div v-else class="tab-pane">
|
<div v-else class="tab-pane">
|
||||||
<div class="form-row mb-md">
|
<div class="form-row mb-md">
|
||||||
<FormCommon
|
<FormCommon
|
||||||
v-model="privateKey"
|
v-model="keystore"
|
||||||
type="text"
|
type="text"
|
||||||
label="Private key"
|
label="Keystore"
|
||||||
placeholder="Enter private key"
|
placeholder="Enter keystore"
|
||||||
:error="privateKeyError"
|
:error="keystoreError"
|
||||||
@focus="privateKeyError = ''"
|
@focus="keystoreError = ''"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -130,7 +127,7 @@ const handleContinue = () => {
|
|||||||
:disabled="
|
:disabled="
|
||||||
tab === 'seedphrase'
|
tab === 'seedphrase'
|
||||||
? !seedWords.every((w) => w) || !!seedError
|
? !seedWords.every((w) => w) || !!seedError
|
||||||
: !privateKey || !!privateKeyError
|
: !keystore || !!keystoreError
|
||||||
"
|
"
|
||||||
@click="handleContinue"
|
@click="handleContinue"
|
||||||
>Continue</ButtonCommon
|
>Continue</ButtonCommon
|
||||||
|
|||||||
@ -12,6 +12,7 @@ const emit = defineEmits<{
|
|||||||
const seedStore = useSeedStore()
|
const seedStore = useSeedStore()
|
||||||
|
|
||||||
const seedWords = ref<string[]>([])
|
const seedWords = ref<string[]>([])
|
||||||
|
const extraWord = ref('')
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const words = generateSeedPhrase()
|
const words = generateSeedPhrase()
|
||||||
@ -55,6 +56,15 @@ const handleBack = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="extra-word-box">
|
||||||
|
<label class="extra-label">Add Extra Word</label>
|
||||||
|
<input
|
||||||
|
class="extra-input"
|
||||||
|
type="text"
|
||||||
|
v-model="extraWord"
|
||||||
|
placeholder="Extra word"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="cool-fact">
|
<div class="cool-fact">
|
||||||
<p>
|
<p>
|
||||||
@ -179,6 +189,29 @@ const handleBack = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.extra-word-box {
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 16px;
|
||||||
|
margin: 14px 0 28px 0;
|
||||||
|
.extra-label {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
.extra-input {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 13px 10px;
|
||||||
|
font-size: 1em;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Responsive Design
|
// Responsive Design
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.recovery-container {
|
.recovery-container {
|
||||||
|
|||||||
@ -131,3 +131,12 @@ export const validateSeedPhrase18 = (words: string[]): boolean => {
|
|||||||
if (words.length !== 18) return false
|
if (words.length !== 18) return false
|
||||||
return words.every((word) => BIP39_WORDS.includes(word.toLowerCase()))
|
return words.every((word) => BIP39_WORDS.includes(word.toLowerCase()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const generateSeedPhrase18 = (): string[] => {
|
||||||
|
const words: string[] = []
|
||||||
|
for (let i = 0; i < 18; i++) {
|
||||||
|
const randomIndex = Math.floor(Math.random() * BIP39_WORDS.length)
|
||||||
|
words.push(BIP39_WORDS[randomIndex])
|
||||||
|
}
|
||||||
|
return words
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user