feat: 311025/login_by_password_and_download_keystore_file_when_creating_wallet
This commit is contained in:
parent
08ed2814c9
commit
f23a20df10
@ -1 +1,2 @@
|
|||||||
VITE_APP_API=
|
VITE_APP_API=
|
||||||
|
VITE_NODE_NETWORK=
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,7 +13,7 @@ dist
|
|||||||
dist-ssr
|
dist-ssr
|
||||||
coverage
|
coverage
|
||||||
*.local
|
*.local
|
||||||
.vite/build/*
|
.vite/*/**
|
||||||
wallets/*
|
wallets/*
|
||||||
|
|
||||||
/cypress/videos/
|
/cypress/videos/
|
||||||
|
|||||||
@ -1,24 +1,56 @@
|
|||||||
import { ipcMain } from 'electron';
|
import { ipcMain } from 'electron'
|
||||||
import type { HDNodeWallet } from 'ethers';
|
import { Wallet } from 'ethers'
|
||||||
import { Wallet } from 'ethers';
|
import fs from 'fs'
|
||||||
import fs from 'fs';
|
import path from 'path'
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
ipcMain.handle('wallet:createKeystore', async (_event, seed, password) => {
|
ipcMain.handle('wallet:createKeystore', async (_event, seed, password) => {
|
||||||
const wallet = Wallet.fromPhrase(seed);
|
try {
|
||||||
const keystore = await wallet.encrypt(password);
|
const wallet = Wallet.fromPhrase(seed)
|
||||||
|
const keystore = await wallet.encrypt(password)
|
||||||
|
|
||||||
const savePath = path.join(process.cwd(), 'wallets');
|
const savePath = path.join(process.cwd(), 'wallets')
|
||||||
fs.mkdirSync(savePath, { recursive: true });
|
fs.mkdirSync(savePath, { recursive: true })
|
||||||
|
|
||||||
const filePath = path.join(savePath, `${wallet.address}.json`);
|
const filePath = path.join(savePath, `${wallet.address}.json`)
|
||||||
fs.writeFileSync(filePath, keystore);
|
fs.writeFileSync(filePath, keystore)
|
||||||
|
|
||||||
return { address: wallet.address, filePath };
|
return { address: wallet.address, filePath, error: null }
|
||||||
});
|
} catch (error) {
|
||||||
|
console.error('Error creating keystore:', error)
|
||||||
|
return { address: null, filePath: null, error: String(error) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('wallet:decryptKeystore', async (_event, filePath, password) => {
|
ipcMain.handle('wallet:decryptKeystore', async (_event, filePath, password) => {
|
||||||
const json = fs.readFileSync(filePath, 'utf-8');
|
try {
|
||||||
const wallet = await Wallet.fromEncryptedJson(json, password) as HDNodeWallet;
|
const json = fs.readFileSync(filePath, 'utf-8')
|
||||||
return { address: wallet.address, phrase: wallet.mnemonic };
|
const wallet = await Wallet.fromEncryptedJson(json, password)
|
||||||
});
|
|
||||||
|
let phrase: string | undefined
|
||||||
|
if ('mnemonic' in wallet && wallet.mnemonic) {
|
||||||
|
phrase = wallet.mnemonic.phrase
|
||||||
|
}
|
||||||
|
|
||||||
|
return { address: wallet.address, phrase, error: null }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error decrypting keystore ipc:', error)
|
||||||
|
return { address: null, phrase: null, error: String(error) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('wallet:checkKeystore', async () => {
|
||||||
|
try {
|
||||||
|
const walletDir = path.join(process.cwd(), 'wallets')
|
||||||
|
if (!fs.existsSync(walletDir))
|
||||||
|
return { exists: false, filePath: null, error: 'Wallet directory not found' }
|
||||||
|
|
||||||
|
const file = fs.readdirSync(walletDir).find((f) => f.endsWith('.json'))
|
||||||
|
if (!file) return { exists: false, filePath: null, error: 'Keystore file not found' }
|
||||||
|
|
||||||
|
const filePath = path.join(walletDir, file)
|
||||||
|
return { exists: true, filePath, error: null }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking keystore:', error)
|
||||||
|
return { exists: false, filePath: null, error: String(error) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@ -7,4 +7,5 @@ contextBridge.exposeInMainWorld('walletApi', {
|
|||||||
ipcRenderer.invoke('wallet:createKeystore', seed, password),
|
ipcRenderer.invoke('wallet:createKeystore', seed, password),
|
||||||
decryptKeystore: (filePath: string, password: string) =>
|
decryptKeystore: (filePath: string, password: string) =>
|
||||||
ipcRenderer.invoke('wallet:decryptKeystore', filePath, password),
|
ipcRenderer.invoke('wallet:decryptKeystore', filePath, password),
|
||||||
|
checkKeystore: () => ipcRenderer.invoke('wallet:checkKeystore'),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -15,6 +15,8 @@ const currentDaaScore = ref(0)
|
|||||||
const isLoadingData = ref(false)
|
const isLoadingData = ref(false)
|
||||||
|
|
||||||
const receiveAddress = computed(() => neptuneStore.getWallet?.address || '')
|
const receiveAddress = computed(() => neptuneStore.getWallet?.address || '')
|
||||||
|
const isAddressExpanded = ref(false)
|
||||||
|
|
||||||
const walletStatus = computed(() => {
|
const walletStatus = computed(() => {
|
||||||
if (neptuneStore.getLoading) return 'Loading...'
|
if (neptuneStore.getLoading) return 'Loading...'
|
||||||
if (neptuneStore.getError) return 'Error'
|
if (neptuneStore.getError) return 'Error'
|
||||||
@ -22,6 +24,10 @@ const walletStatus = computed(() => {
|
|||||||
return 'Offline'
|
return 'Offline'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const toggleAddressExpanded = () => {
|
||||||
|
isAddressExpanded.value = !isAddressExpanded.value
|
||||||
|
}
|
||||||
|
|
||||||
const copyAddress = async () => {
|
const copyAddress = async () => {
|
||||||
if (!receiveAddress.value) {
|
if (!receiveAddress.value) {
|
||||||
message.error('No address available')
|
message.error('No address available')
|
||||||
@ -109,7 +115,11 @@ onMounted(() => {
|
|||||||
<!-- Receive Address Section -->
|
<!-- Receive Address Section -->
|
||||||
<div class="receive-section">
|
<div class="receive-section">
|
||||||
<div class="address-label">Receive Address:</div>
|
<div class="address-label">Receive Address:</div>
|
||||||
<div class="address-value" @click="copyAddress">
|
<div
|
||||||
|
class="address-value"
|
||||||
|
:class="{ expanded: isAddressExpanded, collapsed: !isAddressExpanded }"
|
||||||
|
@click="copyAddress"
|
||||||
|
>
|
||||||
{{ receiveAddress || 'No address available' }}
|
{{ receiveAddress || 'No address available' }}
|
||||||
<svg
|
<svg
|
||||||
class="copy-icon"
|
class="copy-icon"
|
||||||
@ -123,6 +133,13 @@ onMounted(() => {
|
|||||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="receiveAddress && receiveAddress.length > 80"
|
||||||
|
class="toggle-address-btn"
|
||||||
|
@click.stop="toggleAddressExpanded"
|
||||||
|
>
|
||||||
|
{{ isAddressExpanded ? 'Show less' : 'Show more' }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
@ -231,9 +248,26 @@ onMounted(() => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: var(--transition-all);
|
transition: var(--transition-all);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: var(--spacing-sm);
|
gap: var(--spacing-sm);
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
|
line-height: 1.5;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--bg-hover);
|
background: var(--bg-hover);
|
||||||
@ -245,6 +279,29 @@ onMounted(() => {
|
|||||||
height: 18px;
|
height: 18px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
|
margin-top: 2px;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-address-btn {
|
||||||
|
margin-top: var(--spacing-md);
|
||||||
|
padding: var(--spacing-xs) var(--spacing-md);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,12 @@ const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
|
|||||||
const showPassword = ref(false)
|
const showPassword = ref(false)
|
||||||
const isFocused = ref(false)
|
const isFocused = ref(false)
|
||||||
|
|
||||||
const inputType = computed(() => props.type)
|
const inputType = computed(() => {
|
||||||
|
if (props.type === 'password' ) {
|
||||||
|
return showPassword.value ? 'text' : 'password'
|
||||||
|
}
|
||||||
|
return props.type
|
||||||
|
})
|
||||||
|
|
||||||
const togglePassword = () => (showPassword.value = !showPassword.value)
|
const togglePassword = () => (showPassword.value = !showPassword.value)
|
||||||
const handleInput = (e: Event) => emit('update:modelValue', (e.target as HTMLInputElement).value)
|
const handleInput = (e: Event) => emit('update:modelValue', (e.target as HTMLInputElement).value)
|
||||||
@ -56,14 +61,13 @@ const handleBlur = (e: FocusEvent) => {
|
|||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<div
|
||||||
v-if="type === 'password' && showPasswordToggle"
|
v-if="type === 'password' && showPasswordToggle"
|
||||||
type="button"
|
|
||||||
class="password-toggle flex-center"
|
class="password-toggle flex-center"
|
||||||
@click="togglePassword"
|
@click="togglePassword"
|
||||||
>
|
>
|
||||||
<IconCommon :size="20" :icon="showPassword ? 'eye-off' : 'eye'" />
|
<IconCommon :size="20" :icon="showPassword ? 'eye-off' : 'eye'" />
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class="error-message">{{ error }}</div>
|
<div v-if="error" class="error-message">{{ error }}</div>
|
||||||
|
|||||||
133
src/components/common/PasswordForm.vue
Normal file
133
src/components/common/PasswordForm.vue
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { ButtonCommon, FormCommon } from '@/components'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string
|
||||||
|
subtitle?: string
|
||||||
|
buttonText?: string
|
||||||
|
hideBackButton?: boolean
|
||||||
|
backButtonText?: string
|
||||||
|
placeholder?: string
|
||||||
|
label?: string
|
||||||
|
loading?: boolean
|
||||||
|
error?: boolean
|
||||||
|
errorMessage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: 'Access Wallet',
|
||||||
|
subtitle: 'Enter your password to unlock your wallet',
|
||||||
|
buttonText: 'Unlock Wallet',
|
||||||
|
backButtonText: 'Back',
|
||||||
|
hideBackButton: false,
|
||||||
|
placeholder: 'Enter your password',
|
||||||
|
label: 'Password',
|
||||||
|
loading: false,
|
||||||
|
error: false,
|
||||||
|
errorMessage: 'Invalid password',
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
submit: [password: string]
|
||||||
|
back: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const password = ref('')
|
||||||
|
const passwordError = ref('')
|
||||||
|
|
||||||
|
const canProceed = computed(() => {
|
||||||
|
return password.value.length > 0 && !passwordError.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!canProceed.value) {
|
||||||
|
if (!password.value) {
|
||||||
|
passwordError.value = 'Please enter your password'
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
passwordError.value = ''
|
||||||
|
emit('submit', password.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
emit('back')
|
||||||
|
password.value = ''
|
||||||
|
passwordError.value = ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="auth-card-content">
|
||||||
|
<div class="form-group">
|
||||||
|
<FormCommon
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
:label="props.label"
|
||||||
|
:placeholder="props.placeholder"
|
||||||
|
show-password-toggle
|
||||||
|
required
|
||||||
|
:error="passwordError"
|
||||||
|
@input="passwordError = ''"
|
||||||
|
@keyup.enter="handleSubmit"
|
||||||
|
/>
|
||||||
|
<span v-if="error" class="error-message">{{ errorMessage }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="auth-button-group">
|
||||||
|
<ButtonCommon
|
||||||
|
v-if="!props.hideBackButton"
|
||||||
|
type="default"
|
||||||
|
size="large"
|
||||||
|
class="auth-button"
|
||||||
|
block
|
||||||
|
@click="handleBack"
|
||||||
|
>
|
||||||
|
{{ props.backButtonText }}
|
||||||
|
</ButtonCommon>
|
||||||
|
<ButtonCommon
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
class="auth-button"
|
||||||
|
block
|
||||||
|
:disabled="!canProceed"
|
||||||
|
:loading="props.loading"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
{{ props.buttonText }}
|
||||||
|
</ButtonCommon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.auth-card-content {
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-button {
|
||||||
|
width: fit-content;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-button-group {
|
||||||
|
width: 50%;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-top: var(--spacing-2xl);
|
||||||
|
@include center_flex;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
display: block;
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
background: var(--error-light);
|
||||||
|
color: var(--error-color);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import LayoutVue from './common/LayoutVue.vue'
|
import LayoutVue from './common/LayoutVue.vue'
|
||||||
import ButtonCommon from './common/ButtonCommon.vue'
|
import ButtonCommon from './common/ButtonCommon.vue'
|
||||||
import FormCommon from './common/FormCommon.vue'
|
import FormCommon from './common/FormCommon.vue'
|
||||||
|
import PasswordForm from './common/PasswordForm.vue'
|
||||||
import SpinnerCommon from './common/SpinnerCommon.vue'
|
import SpinnerCommon from './common/SpinnerCommon.vue'
|
||||||
import { IconCommon } from './icon'
|
import { IconCommon } from './icon'
|
||||||
|
|
||||||
export { LayoutVue, ButtonCommon, FormCommon, SpinnerCommon, IconCommon }
|
export { LayoutVue, ButtonCommon, FormCommon, PasswordForm, SpinnerCommon, IconCommon }
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { computed } from 'vue'
|
|
||||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||||
import * as API from '@/api/neptuneApi'
|
import * as API from '@/api/neptuneApi'
|
||||||
import type { GenerateSeedResult, ViewKeyResult } from '@/interface'
|
import type { GenerateSeedResult, ViewKeyResult } from '@/interface'
|
||||||
@ -7,7 +6,6 @@ import initWasm, {
|
|||||||
get_viewkey,
|
get_viewkey,
|
||||||
address_from_seed,
|
address_from_seed,
|
||||||
validate_seed_phrase,
|
validate_seed_phrase,
|
||||||
decode_viewkey,
|
|
||||||
} from '@neptune/wasm'
|
} from '@neptune/wasm'
|
||||||
|
|
||||||
let wasmInitialized = false
|
let wasmInitialized = false
|
||||||
@ -57,13 +55,11 @@ export function useNeptuneWallet() {
|
|||||||
|
|
||||||
const resultJson = generate_seed()
|
const resultJson = generate_seed()
|
||||||
const result: GenerateSeedResult = JSON.parse(resultJson)
|
const result: GenerateSeedResult = JSON.parse(resultJson)
|
||||||
console.log('result :>> ', result);
|
|
||||||
|
|
||||||
store.setSeedPhrase(result.seed_phrase)
|
store.setSeedPhrase(result.seed_phrase)
|
||||||
store.setReceiverId(result.receiver_identifier)
|
store.setReceiverId(result.receiver_identifier)
|
||||||
|
|
||||||
const viewKeyResult = await getViewKeyFromSeed(result.seed_phrase, store.getNetwork)
|
const viewKeyResult = await getViewKeyFromSeed(result.seed_phrase)
|
||||||
|
|
||||||
store.setViewKey(viewKeyResult.view_key)
|
store.setViewKey(viewKeyResult.view_key)
|
||||||
store.setAddress(viewKeyResult.address)
|
store.setAddress(viewKeyResult.address)
|
||||||
|
|
||||||
@ -77,20 +73,14 @@ export function useNeptuneWallet() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getViewKeyFromSeed = async (
|
const getViewKeyFromSeed = async (seedPhrase: string[]): Promise<ViewKeyResult> => {
|
||||||
seedPhrase: string[],
|
|
||||||
network: 'mainnet' | 'testnet'
|
|
||||||
): Promise<ViewKeyResult> => {
|
|
||||||
await ensureWasmInitialized()
|
await ensureWasmInitialized()
|
||||||
const seedPhraseJson = JSON.stringify(seedPhrase)
|
const seedPhraseJson = JSON.stringify(seedPhrase)
|
||||||
const resultJson = get_viewkey(seedPhraseJson, network)
|
const resultJson = get_viewkey(seedPhraseJson, store.getNetwork)
|
||||||
return JSON.parse(resultJson)
|
return JSON.parse(resultJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
const importWallet = async (
|
const importWallet = async (seedPhrase: string[]): Promise<ViewKeyResult> => {
|
||||||
seedPhrase: string[],
|
|
||||||
network: 'mainnet' | 'testnet' = 'testnet'
|
|
||||||
): Promise<ViewKeyResult> => {
|
|
||||||
try {
|
try {
|
||||||
store.setLoading(true)
|
store.setLoading(true)
|
||||||
store.setError(null)
|
store.setError(null)
|
||||||
@ -100,13 +90,12 @@ export function useNeptuneWallet() {
|
|||||||
throw new Error('Invalid seed phrase')
|
throw new Error('Invalid seed phrase')
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await getViewKeyFromSeed(seedPhrase, network)
|
const result = await getViewKeyFromSeed(seedPhrase)
|
||||||
|
|
||||||
store.setSeedPhrase(seedPhrase)
|
store.setSeedPhrase(seedPhrase)
|
||||||
store.setReceiverId(result.receiver_identifier)
|
store.setReceiverId(result.receiver_identifier)
|
||||||
store.setViewKey(result.view_key)
|
store.setViewKey(result.view_key)
|
||||||
store.setAddress(result.address)
|
store.setAddress(result.address)
|
||||||
store.setNetwork(network)
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -118,13 +107,10 @@ export function useNeptuneWallet() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAddressFromSeed = async (
|
const getAddressFromSeed = async (seedPhrase: string[]): Promise<string> => {
|
||||||
seedPhrase: string[],
|
|
||||||
network: 'mainnet' | 'testnet'
|
|
||||||
): Promise<string> => {
|
|
||||||
await ensureWasmInitialized()
|
await ensureWasmInitialized()
|
||||||
const seedPhraseJson = JSON.stringify(seedPhrase)
|
const seedPhraseJson = JSON.stringify(seedPhrase)
|
||||||
return address_from_seed(seedPhraseJson, network)
|
return address_from_seed(seedPhraseJson, store.getNetwork)
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateSeedPhrase = async (seedPhrase: string[]): Promise<boolean> => {
|
const validateSeedPhrase = async (seedPhrase: string[]): Promise<boolean> => {
|
||||||
@ -138,10 +124,27 @@ export function useNeptuneWallet() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const decodeViewKey = async (viewKeyHex: string): Promise<{ receiver_identifier: string }> => {
|
const decryptKeystore = async (password: string): Promise<void> => {
|
||||||
await ensureWasmInitialized()
|
try {
|
||||||
const resultJson = decode_viewkey(viewKeyHex)
|
const result = await (window as any).walletApi.decryptKeystore(
|
||||||
return JSON.parse(resultJson)
|
store.getKeystorePath || '',
|
||||||
|
password
|
||||||
|
)
|
||||||
|
if (result.error) {
|
||||||
|
console.error('Error decrypting keystore composable:', result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const seedPhrase = result.phrase.trim().split(/\s+/)
|
||||||
|
const viewKeyResult = await getViewKeyFromSeed(seedPhrase)
|
||||||
|
|
||||||
|
store.setSeedPhrase(seedPhrase)
|
||||||
|
store.setAddress(viewKeyResult.address)
|
||||||
|
store.setViewKey(viewKeyResult.view_key)
|
||||||
|
store.setReceiverId(viewKeyResult.receiver_identifier)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error decrypting keystore composable:', err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== API METHODS =====
|
// ===== API METHODS =====
|
||||||
@ -256,7 +259,7 @@ export function useNeptuneWallet() {
|
|||||||
store.setNetwork(network)
|
store.setNetwork(network)
|
||||||
|
|
||||||
if (store.getSeedPhrase) {
|
if (store.getSeedPhrase) {
|
||||||
const viewKeyResult = await getViewKeyFromSeed(store.getSeedPhrase, network)
|
const viewKeyResult = await getViewKeyFromSeed(store.getSeedPhrase)
|
||||||
store.setAddress(viewKeyResult.address)
|
store.setAddress(viewKeyResult.address)
|
||||||
store.setViewKey(viewKeyResult.view_key)
|
store.setViewKey(viewKeyResult.view_key)
|
||||||
}
|
}
|
||||||
@ -268,24 +271,19 @@ export function useNeptuneWallet() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
walletState: computed(() => store.getWallet),
|
|
||||||
isLoading: computed(() => store.getLoading),
|
|
||||||
error: computed(() => store.getError),
|
|
||||||
hasWallet: computed(() => store.hasWallet),
|
|
||||||
|
|
||||||
initWasm: ensureWasmInitialized,
|
initWasm: ensureWasmInitialized,
|
||||||
generateWallet,
|
generateWallet,
|
||||||
importWallet,
|
importWallet,
|
||||||
getViewKeyFromSeed,
|
getViewKeyFromSeed,
|
||||||
getAddressFromSeed,
|
getAddressFromSeed,
|
||||||
validateSeedPhrase,
|
validateSeedPhrase,
|
||||||
decodeViewKey,
|
|
||||||
|
|
||||||
getUtxos,
|
getUtxos,
|
||||||
getBalance,
|
getBalance,
|
||||||
getBlockHeight,
|
getBlockHeight,
|
||||||
getNetworkInfo,
|
getNetworkInfo,
|
||||||
sendTransaction,
|
sendTransaction,
|
||||||
|
decryptKeystore,
|
||||||
|
|
||||||
clearWallet,
|
clearWallet,
|
||||||
setNetwork,
|
setNetwork,
|
||||||
|
|||||||
@ -1,19 +1,30 @@
|
|||||||
import * as Page from '@/views'
|
import * as Page from '@/views'
|
||||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||||
import { useAuthStore } from '@/stores/authStore'
|
import { useAuthStore } from '@/stores/authStore'
|
||||||
|
const ifAuthenticated = (to: any, from: any, next: any) => {
|
||||||
export const ifAuthenticated = (to: any, from: any, next: any) => {
|
|
||||||
const neptuneStore = useNeptuneStore()
|
const neptuneStore = useNeptuneStore()
|
||||||
const authStore = useAuthStore()
|
|
||||||
|
|
||||||
if (neptuneStore.getReceiverId) {
|
if (neptuneStore.hasWallet) {
|
||||||
next()
|
next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
authStore.setState('login')
|
|
||||||
next('/auth')
|
next('/auth')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const ifNotAuthenticated = async (to: any, from: any, next: any) => {
|
||||||
|
const neptuneStore = useNeptuneStore()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const keystoreFile = await (window as any).walletApi.checkKeystore()
|
||||||
|
if (keystoreFile.exists) {
|
||||||
|
neptuneStore.setKeystorePath(keystoreFile.filePath)
|
||||||
|
authStore.setState('login')
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
export const routes: any = [
|
export const routes: any = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
@ -25,6 +36,7 @@ export const routes: any = [
|
|||||||
path: '/auth',
|
path: '/auth',
|
||||||
name: 'auth',
|
name: 'auth',
|
||||||
component: Page.Auth,
|
component: Page.Auth,
|
||||||
|
beforeEnter: ifNotAuthenticated,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { ref, computed } from 'vue'
|
|||||||
import type { WalletState } from '@/interface'
|
import type { WalletState } from '@/interface'
|
||||||
|
|
||||||
export const useNeptuneStore = defineStore('neptune', () => {
|
export const useNeptuneStore = defineStore('neptune', () => {
|
||||||
|
const defaultNetwork = (import.meta.env.VITE_NODE_NETWORK || 'testnet') as 'mainnet' | 'testnet'
|
||||||
|
|
||||||
// ===== STATE =====
|
// ===== STATE =====
|
||||||
const wallet = ref<WalletState>({
|
const wallet = ref<WalletState>({
|
||||||
seedPhrase: null,
|
seedPhrase: null,
|
||||||
@ -10,11 +12,13 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
receiverId: null,
|
receiverId: null,
|
||||||
viewKey: null,
|
viewKey: null,
|
||||||
address: null,
|
address: null,
|
||||||
network: 'testnet',
|
network: defaultNetwork,
|
||||||
balance: null,
|
balance: null,
|
||||||
utxos: [],
|
utxos: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const keystorePath = ref<null | string>(null)
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
@ -64,6 +68,10 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
wallet.value = { ...wallet.value, ...walletData }
|
wallet.value = { ...wallet.value, ...walletData }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setKeystorePath = (path: string | null) => {
|
||||||
|
keystorePath.value = path
|
||||||
|
}
|
||||||
|
|
||||||
const clearWallet = () => {
|
const clearWallet = () => {
|
||||||
wallet.value = {
|
wallet.value = {
|
||||||
seedPhrase: null,
|
seedPhrase: null,
|
||||||
@ -71,7 +79,7 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
receiverId: null,
|
receiverId: null,
|
||||||
viewKey: null,
|
viewKey: null,
|
||||||
address: null,
|
address: null,
|
||||||
network: 'testnet',
|
network: defaultNetwork,
|
||||||
balance: null,
|
balance: null,
|
||||||
utxos: [],
|
utxos: [],
|
||||||
}
|
}
|
||||||
@ -92,7 +100,7 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
const hasWallet = computed(() => wallet.value.address !== null)
|
const hasWallet = computed(() => wallet.value.address !== null)
|
||||||
const getLoading = computed(() => isLoading.value)
|
const getLoading = computed(() => isLoading.value)
|
||||||
const getError = computed(() => error.value)
|
const getError = computed(() => error.value)
|
||||||
|
const getKeystorePath = computed(() => keystorePath.value)
|
||||||
return {
|
return {
|
||||||
getWallet,
|
getWallet,
|
||||||
getSeedPhrase,
|
getSeedPhrase,
|
||||||
@ -107,7 +115,7 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
hasWallet,
|
hasWallet,
|
||||||
getLoading,
|
getLoading,
|
||||||
getError,
|
getError,
|
||||||
|
getKeystorePath,
|
||||||
setSeedPhrase,
|
setSeedPhrase,
|
||||||
setPassword,
|
setPassword,
|
||||||
setReceiverId,
|
setReceiverId,
|
||||||
@ -119,6 +127,7 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
setLoading,
|
setLoading,
|
||||||
setError,
|
setError,
|
||||||
setWallet,
|
setWallet,
|
||||||
|
setKeystorePath,
|
||||||
clearWallet,
|
clearWallet,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
16
src/types/electron.d.ts
vendored
16
src/types/electron.d.ts
vendored
@ -1,12 +1,12 @@
|
|||||||
// Global type definitions for Electron API
|
// Global type definitions for Electron API
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
electron: {
|
electron: {
|
||||||
send: (channel: string, data: any) => void;
|
send: (channel: string, data: any) => void
|
||||||
receive: (channel: string, func: (...args: any[]) => void) => void;
|
receive: (channel: string, func: (...args: any[]) => void) => void
|
||||||
removeAllListeners: (channel: string) => void;
|
removeAllListeners: (channel: string) => void
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {}
|
||||||
|
|||||||
@ -12,9 +12,7 @@ const handleNavigateToRecoverWallet = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="create-tab">
|
<div class="create-tab">
|
||||||
<CreateWalletComponent
|
<CreateWalletComponent @navigate-to-recover-wallet="handleNavigateToRecoverWallet" />
|
||||||
@navigate-to-recover-wallet="handleNavigateToRecoverWallet"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,236 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { PasswordForm } from '@/components'
|
||||||
|
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useAuthStore } from '@/stores/authStore'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const neptuneWallet = useNeptuneWallet()
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const error = ref(false)
|
||||||
|
|
||||||
|
const handlePasswordSubmit = async (password: string) => {
|
||||||
|
try {
|
||||||
|
isLoading.value = true
|
||||||
|
await neptuneWallet.decryptKeystore(password)
|
||||||
|
router.push({name: 'home'})
|
||||||
|
} catch (err) {
|
||||||
|
error.value = true
|
||||||
|
console.error('Password Error')
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNewWallet = () => {
|
||||||
|
authStore.goToCreate()
|
||||||
|
router.push({name: 'auth'})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="login-tab">
|
<div class="login-tab">
|
||||||
|
<div class="login-password-form">
|
||||||
|
<div class="auth-card-header">
|
||||||
|
<div class="logo-container">
|
||||||
|
<div class="logo-circle">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
class="neptune-logo"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id="neptuneGradientLogin"
|
||||||
|
x1="0%"
|
||||||
|
y1="0%"
|
||||||
|
x2="100%"
|
||||||
|
y2="100%"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset="0%"
|
||||||
|
style="stop-color: #007fcf; stop-opacity: 1"
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset="100%"
|
||||||
|
style="stop-color: #0066a6; stop-opacity: 1"
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="ringGradientLogin"
|
||||||
|
x1="0%"
|
||||||
|
y1="0%"
|
||||||
|
x2="100%"
|
||||||
|
y2="0%"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset="0%"
|
||||||
|
style="stop-color: #007fcf; stop-opacity: 0.3"
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset="50%"
|
||||||
|
style="stop-color: #007fcf; stop-opacity: 0.6"
|
||||||
|
/>
|
||||||
|
<stop
|
||||||
|
offset="100%"
|
||||||
|
style="stop-color: #007fcf; stop-opacity: 0.3"
|
||||||
|
/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<circle cx="50" cy="50" r="28" fill="url(#neptuneGradientLogin)" />
|
||||||
|
|
||||||
|
<ellipse
|
||||||
|
cx="50"
|
||||||
|
cy="45"
|
||||||
|
rx="22"
|
||||||
|
ry="6"
|
||||||
|
fill="rgba(255, 255, 255, 0.1)"
|
||||||
|
/>
|
||||||
|
<ellipse cx="50" cy="55" rx="20" ry="5" fill="rgba(0, 0, 0, 0.1)" />
|
||||||
|
|
||||||
|
<ellipse
|
||||||
|
cx="50"
|
||||||
|
cy="50"
|
||||||
|
rx="42"
|
||||||
|
ry="12"
|
||||||
|
fill="none"
|
||||||
|
stroke="url(#ringGradientLogin)"
|
||||||
|
stroke-width="4"
|
||||||
|
opacity="0.8"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<circle cx="42" cy="42" r="6" fill="rgba(255, 255, 255, 0.4)" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="logo-text">
|
||||||
|
<span class="coin-name">Neptune</span>
|
||||||
|
<span class="coin-symbol">NPT</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 class="auth-title">Access Wallet</h1>
|
||||||
|
<p class="auth-subtitle">Enter your password to unlock your wallet</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PasswordForm
|
||||||
|
button-text="Unlock Wallet"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
back-button-text="New Wallet"
|
||||||
|
label="Password"
|
||||||
|
:loading="isLoading"
|
||||||
|
@submit="handlePasswordSubmit"
|
||||||
|
@back="handleNewWallet"
|
||||||
|
:error="error"
|
||||||
|
:errorMessage="'Invalid password'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.login-tab {
|
.login-tab {
|
||||||
padding: var(--spacing-lg);
|
padding: var(--spacing-lg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-password-form {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--spacing-2xl);
|
||||||
|
padding-bottom: var(--spacing-xl);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
|
||||||
|
.logo-circle {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, var(--primary-light), var(--bg-white));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 127, 207, 0.15);
|
||||||
|
|
||||||
|
.neptune-logo {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.coin-name {
|
||||||
|
font-size: var(--font-lg);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coin-symbol {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--primary-color);
|
||||||
|
background: var(--primary-light);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-title {
|
||||||
|
font-size: var(--font-2xl);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-subtitle {
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include screen(mobile) {
|
||||||
|
.auth-card-header {
|
||||||
|
.logo-container {
|
||||||
|
.logo-circle {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
.coin-name {
|
||||||
|
font-size: var(--font-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-title {
|
||||||
|
font-size: var(--font-xl);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -31,7 +31,7 @@ const handleRecover = () => {
|
|||||||
Create new wallet
|
Create new wallet
|
||||||
</ButtonCommon>
|
</ButtonCommon>
|
||||||
<ButtonCommon type="default" size="large" @click="handleRecover">
|
<ButtonCommon type="default" size="large" @click="handleRecover">
|
||||||
Recover wallet
|
Recover wallet
|
||||||
</ButtonCommon>
|
</ButtonCommon>
|
||||||
</div>
|
</div>
|
||||||
<div class="note">Thank you for being a part of the Neptune community!</div>
|
<div class="note">Thank you for being a part of the Neptune community!</div>
|
||||||
|
|||||||
@ -83,28 +83,28 @@ const handleAnswerSelect = (answer: string) => {
|
|||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
emit('next')
|
emit('next')
|
||||||
// if (isCorrect.value) {
|
if (isCorrect.value) {
|
||||||
// correctCount.value++
|
correctCount.value++
|
||||||
// askedPositions.value.add(quizData.value!.position)
|
askedPositions.value.add(quizData.value!.position)
|
||||||
|
|
||||||
// if (correctCount.value >= totalQuestions) {
|
if (correctCount.value >= totalQuestions) {
|
||||||
// emit('next')
|
emit('next')
|
||||||
// } else {
|
} else {
|
||||||
// showResult.value = false
|
showResult.value = false
|
||||||
// selectedAnswer.value = ''
|
selectedAnswer.value = ''
|
||||||
// const newQuiz = generateQuiz()
|
const newQuiz = generateQuiz()
|
||||||
// if (newQuiz) {
|
if (newQuiz) {
|
||||||
// quizData.value = newQuiz
|
quizData.value = newQuiz
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// } else {
|
} else {
|
||||||
// showResult.value = false
|
showResult.value = false
|
||||||
// selectedAnswer.value = ''
|
selectedAnswer.value = ''
|
||||||
// const newQuiz = generateQuiz()
|
const newQuiz = generateQuiz()
|
||||||
// if (newQuiz) {
|
if (newQuiz) {
|
||||||
// quizData.value = newQuiz
|
quizData.value = newQuiz
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
@ -216,11 +216,7 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
CONTINUE
|
CONTINUE
|
||||||
</ButtonCommon> -->
|
</ButtonCommon> -->
|
||||||
<ButtonCommon
|
<ButtonCommon type="primary" size="large" @click="handleNext">
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
@click="handleNext"
|
|
||||||
>
|
|
||||||
CONTINUE
|
CONTINUE
|
||||||
</ButtonCommon>
|
</ButtonCommon>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { ButtonCommon, FormCommon } from '@/components'
|
import { ButtonCommon, FormCommon } from '@/components'
|
||||||
import { useNeptuneStore } from '@/stores';
|
import { useNeptuneStore } from '@/stores'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
next: []
|
next: []
|
||||||
navigateToOpenWallet: [event: Event]
|
navigateToRecoverWallet: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const neptuneStore = useNeptuneStore()
|
const neptuneStore = useNeptuneStore()
|
||||||
@ -58,13 +58,11 @@ const handleNext = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
neptuneStore.setPassword(password.value)
|
neptuneStore.setPassword(password.value)
|
||||||
// console.log('neptuneStore.getPassword :>> ', neptuneStore.getPassword)
|
|
||||||
emit('next')
|
emit('next')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleIHaveWallet = (e: Event) => {
|
const handleIHaveWallet = () => {
|
||||||
e.preventDefault()
|
emit('navigateToRecoverWallet')
|
||||||
emit('navigateToOpenWallet', e)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -192,9 +190,14 @@ const handleIHaveWallet = (e: Event) => {
|
|||||||
Create Wallet
|
Create Wallet
|
||||||
</ButtonCommon>
|
</ButtonCommon>
|
||||||
<div class="secondary-actions">
|
<div class="secondary-actions">
|
||||||
<button class="link-button" @click="handleIHaveWallet">
|
<ButtonCommon
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
class="link-button"
|
||||||
|
@click="handleIHaveWallet"
|
||||||
|
>
|
||||||
Already have a wallet?
|
Already have a wallet?
|
||||||
</button>
|
</ButtonCommon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonCommon } from '@/components'
|
import { ButtonCommon } from '@/components'
|
||||||
import { useNeptuneStore } from '@/stores/neptuneStore';
|
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||||
|
|
||||||
const neptuneStore = useNeptuneStore()
|
const neptuneStore = useNeptuneStore()
|
||||||
|
|
||||||
@ -13,8 +13,6 @@ const handleAccessWallet = async () => {
|
|||||||
const seedPhrase = neptuneStore.getSeedPhraseString
|
const seedPhrase = neptuneStore.getSeedPhraseString
|
||||||
const password = neptuneStore.getPassword!
|
const password = neptuneStore.getPassword!
|
||||||
const encrypted = (window as any).walletApi.createKeystore(seedPhrase, password)
|
const encrypted = (window as any).walletApi.createKeystore(seedPhrase, password)
|
||||||
console.log('encrypted keystore sample:', encrypted)
|
|
||||||
// TODO: save keystore file, update settings.json, clear RAM... (implement in later steps)
|
|
||||||
emit('accessWallet')
|
emit('accessWallet')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,29 +1,51 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { ButtonCommon } from '@/components'
|
import { Modal } from 'ant-design-vue'
|
||||||
|
import { ButtonCommon, PasswordForm } from '@/components'
|
||||||
import { RecoverSeedComponent } from '..'
|
import { RecoverSeedComponent } from '..'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'import-success', data: { type: 'seed'; value: string[] }): void
|
(e: 'import-success', data: { type: 'seed'; value: string[]; password: string }): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const recoverSeedComponentRef = ref<InstanceType<typeof RecoverSeedComponent>>()
|
const recoverSeedComponentRef = ref<InstanceType<typeof RecoverSeedComponent>>()
|
||||||
const isSeedPhraseValid = ref(false)
|
const isSeedPhraseValid = ref(false)
|
||||||
|
const showPasswordModal = ref(false)
|
||||||
|
const seedPhrase = ref<string[]>([])
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const passwordError = ref(false)
|
||||||
|
|
||||||
const handleSeedPhraseSubmit = (words: string[]) => {
|
const handleSeedPhraseSubmit = (words: string[]) => {
|
||||||
emit('import-success', {
|
seedPhrase.value = words
|
||||||
type: 'seed',
|
showPasswordModal.value = true
|
||||||
value: words,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleContinue = () => {
|
const handleContinue = () => {
|
||||||
recoverSeedComponentRef.value?.handleSubmit?.()
|
recoverSeedComponentRef.value?.handleSubmit?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlePasswordSubmit = (password: string) => {
|
||||||
|
showPasswordModal.value = false
|
||||||
|
emit('import-success', {
|
||||||
|
type: 'seed',
|
||||||
|
value: seedPhrase.value,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePasswordBack = () => {
|
||||||
|
showPasswordModal.value = false
|
||||||
|
passwordError.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleModalCancel = () => {
|
||||||
|
showPasswordModal.value = false
|
||||||
|
passwordError.value = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="import-wallet dark-card">
|
<div class="import-wallet dark-card">
|
||||||
<h2 class="title">Import Wallet</h2>
|
<h2 class="title">Recover Wallet</h2>
|
||||||
<div class="desc">Enter your recovery seed phrase</div>
|
<div class="desc">Enter your recovery seed phrase</div>
|
||||||
<RecoverSeedComponent
|
<RecoverSeedComponent
|
||||||
ref="recoverSeedComponentRef"
|
ref="recoverSeedComponentRef"
|
||||||
@ -40,6 +62,27 @@ const handleContinue = () => {
|
|||||||
>Continue</ButtonCommon
|
>Continue</ButtonCommon
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
v-model:open="showPasswordModal"
|
||||||
|
title="Enter Password"
|
||||||
|
:footer="null"
|
||||||
|
:width="480"
|
||||||
|
:mask-closable="false"
|
||||||
|
:keyboard="false"
|
||||||
|
@cancel="handleModalCancel"
|
||||||
|
>
|
||||||
|
<PasswordForm
|
||||||
|
button-text="Continue"
|
||||||
|
placeholder="Enter password to encrypt seed phrase"
|
||||||
|
label="Password"
|
||||||
|
:loading="isLoading"
|
||||||
|
:error="passwordError"
|
||||||
|
:error-message="'Invalid password'"
|
||||||
|
@submit="handlePasswordSubmit"
|
||||||
|
@back="handlePasswordBack"
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.import-wallet {
|
.import-wallet {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { SpinnerCommon } from '@/components'
|
|||||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||||
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
|
import { get_network_info } from '@neptune/wasm'
|
||||||
|
|
||||||
const neptuneStore = useNeptuneStore()
|
const neptuneStore = useNeptuneStore()
|
||||||
const { getBlockHeight, getNetworkInfo } = useNeptuneWallet()
|
const { getBlockHeight, getNetworkInfo } = useNeptuneWallet()
|
||||||
@ -22,7 +23,6 @@ const network = computed(() => neptuneStore.getNetwork)
|
|||||||
const loadNetworkData = async () => {
|
const loadNetworkData = async () => {
|
||||||
try {
|
try {
|
||||||
error.value = ''
|
error.value = ''
|
||||||
|
|
||||||
const [heightResult, infoResult] = await Promise.all([getBlockHeight(), getNetworkInfo()])
|
const [heightResult, infoResult] = await Promise.all([getBlockHeight(), getNetworkInfo()])
|
||||||
|
|
||||||
if (heightResult !== undefined) {
|
if (heightResult !== undefined) {
|
||||||
|
|||||||
@ -1,13 +1,31 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Divider } from 'ant-design-vue'
|
import { ref } from 'vue'
|
||||||
|
import { Divider, Modal } from 'ant-design-vue'
|
||||||
import ButtonCommon from '@/components/common/ButtonCommon.vue'
|
import ButtonCommon from '@/components/common/ButtonCommon.vue'
|
||||||
|
import SeedPhraseDisplayComponent from '@/views/Auth/components/SeedPhraseDisplayComponent.vue'
|
||||||
|
|
||||||
|
const showSeedModal = ref(false)
|
||||||
|
|
||||||
const handleBackupFile = () => {
|
const handleBackupFile = () => {
|
||||||
// TODO: Implement backup file functionality
|
// TODO: Implement backup file functionality
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBackupSeed = () => {
|
const handleBackupSeed = () => {
|
||||||
// TODO: Implement backup seed functionality
|
showSeedModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
showSeedModal.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleModalNext = () => {
|
||||||
|
// SeedPhraseDisplayComponent emits 'next' but in modal context, we just close
|
||||||
|
handleCloseModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleModalBack = () => {
|
||||||
|
// SeedPhraseDisplayComponent emits 'back' but in modal context, we just close
|
||||||
|
handleCloseModal()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -28,6 +46,20 @@ const handleBackupSeed = () => {
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
v-model:open="showSeedModal"
|
||||||
|
title="Backup Seed Phrase"
|
||||||
|
:footer="null"
|
||||||
|
:width="600"
|
||||||
|
:mask-closable="false"
|
||||||
|
:keyboard="false"
|
||||||
|
@cancel="handleCloseModal"
|
||||||
|
>
|
||||||
|
<div class="seed-modal-content">
|
||||||
|
<SeedPhraseDisplayComponent @next="handleModalNext" @back="handleModalBack" />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -91,4 +123,18 @@ const handleBackupSeed = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.seed-modal-content {
|
||||||
|
:deep(.recovery-container) {
|
||||||
|
padding: 0;
|
||||||
|
min-height: auto;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.recovery-card) {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user