feat: keystore and recovery seed

This commit is contained in:
NguyenAnhQuan 2025-10-23 18:36:58 +07:00
parent 3040bd5a57
commit ecbcc08209
4 changed files with 141 additions and 24 deletions

View File

@ -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);

View File

@ -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

View File

@ -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 {

View File

@ -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
}