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_NODE_NETWORK=
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,7 +13,7 @@ dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
.vite/build/*
|
||||
.vite/*/**
|
||||
wallets/*
|
||||
|
||||
/cypress/videos/
|
||||
|
||||
@ -1,24 +1,56 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import type { HDNodeWallet } from 'ethers';
|
||||
import { Wallet } from 'ethers';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { ipcMain } from 'electron'
|
||||
import { Wallet } from 'ethers'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
ipcMain.handle('wallet:createKeystore', async (_event, seed, password) => {
|
||||
const wallet = Wallet.fromPhrase(seed);
|
||||
const keystore = await wallet.encrypt(password);
|
||||
try {
|
||||
const wallet = Wallet.fromPhrase(seed)
|
||||
const keystore = await wallet.encrypt(password)
|
||||
|
||||
const savePath = path.join(process.cwd(), 'wallets');
|
||||
fs.mkdirSync(savePath, { recursive: true });
|
||||
const savePath = path.join(process.cwd(), 'wallets')
|
||||
fs.mkdirSync(savePath, { recursive: true })
|
||||
|
||||
const filePath = path.join(savePath, `${wallet.address}.json`);
|
||||
fs.writeFileSync(filePath, keystore);
|
||||
const filePath = path.join(savePath, `${wallet.address}.json`)
|
||||
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) => {
|
||||
const json = fs.readFileSync(filePath, 'utf-8');
|
||||
const wallet = await Wallet.fromEncryptedJson(json, password) as HDNodeWallet;
|
||||
return { address: wallet.address, phrase: wallet.mnemonic };
|
||||
});
|
||||
try {
|
||||
const json = fs.readFileSync(filePath, 'utf-8')
|
||||
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),
|
||||
decryptKeystore: (filePath: string, password: string) =>
|
||||
ipcRenderer.invoke('wallet:decryptKeystore', filePath, password),
|
||||
checkKeystore: () => ipcRenderer.invoke('wallet:checkKeystore'),
|
||||
})
|
||||
|
||||
@ -15,6 +15,8 @@ const currentDaaScore = ref(0)
|
||||
const isLoadingData = ref(false)
|
||||
|
||||
const receiveAddress = computed(() => neptuneStore.getWallet?.address || '')
|
||||
const isAddressExpanded = ref(false)
|
||||
|
||||
const walletStatus = computed(() => {
|
||||
if (neptuneStore.getLoading) return 'Loading...'
|
||||
if (neptuneStore.getError) return 'Error'
|
||||
@ -22,6 +24,10 @@ const walletStatus = computed(() => {
|
||||
return 'Offline'
|
||||
})
|
||||
|
||||
const toggleAddressExpanded = () => {
|
||||
isAddressExpanded.value = !isAddressExpanded.value
|
||||
}
|
||||
|
||||
const copyAddress = async () => {
|
||||
if (!receiveAddress.value) {
|
||||
message.error('No address available')
|
||||
@ -109,7 +115,11 @@ onMounted(() => {
|
||||
<!-- Receive Address Section -->
|
||||
<div class="receive-section">
|
||||
<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' }}
|
||||
<svg
|
||||
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>
|
||||
</svg>
|
||||
</div>
|
||||
<button
|
||||
v-if="receiveAddress && receiveAddress.length > 80"
|
||||
class="toggle-address-btn"
|
||||
@click.stop="toggleAddressExpanded"
|
||||
>
|
||||
{{ isAddressExpanded ? 'Show less' : 'Show more' }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
@ -231,9 +248,26 @@ onMounted(() => {
|
||||
cursor: pointer;
|
||||
transition: var(--transition-all);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-sm);
|
||||
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 {
|
||||
background: var(--bg-hover);
|
||||
@ -245,6 +279,29 @@ onMounted(() => {
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
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 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 handleInput = (e: Event) => emit('update:modelValue', (e.target as HTMLInputElement).value)
|
||||
@ -56,14 +61,13 @@ const handleBlur = (e: FocusEvent) => {
|
||||
@blur="handleBlur"
|
||||
/>
|
||||
|
||||
<button
|
||||
<div
|
||||
v-if="type === 'password' && showPasswordToggle"
|
||||
type="button"
|
||||
class="password-toggle flex-center"
|
||||
@click="togglePassword"
|
||||
>
|
||||
<IconCommon :size="20" :icon="showPassword ? 'eye-off' : 'eye'" />
|
||||
</button>
|
||||
</div>
|
||||
</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 ButtonCommon from './common/ButtonCommon.vue'
|
||||
import FormCommon from './common/FormCommon.vue'
|
||||
import PasswordForm from './common/PasswordForm.vue'
|
||||
import SpinnerCommon from './common/SpinnerCommon.vue'
|
||||
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 * as API from '@/api/neptuneApi'
|
||||
import type { GenerateSeedResult, ViewKeyResult } from '@/interface'
|
||||
@ -7,7 +6,6 @@ import initWasm, {
|
||||
get_viewkey,
|
||||
address_from_seed,
|
||||
validate_seed_phrase,
|
||||
decode_viewkey,
|
||||
} from '@neptune/wasm'
|
||||
|
||||
let wasmInitialized = false
|
||||
@ -57,13 +55,11 @@ export function useNeptuneWallet() {
|
||||
|
||||
const resultJson = generate_seed()
|
||||
const result: GenerateSeedResult = JSON.parse(resultJson)
|
||||
console.log('result :>> ', result);
|
||||
|
||||
store.setSeedPhrase(result.seed_phrase)
|
||||
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.setAddress(viewKeyResult.address)
|
||||
|
||||
@ -77,20 +73,14 @@ export function useNeptuneWallet() {
|
||||
}
|
||||
}
|
||||
|
||||
const getViewKeyFromSeed = async (
|
||||
seedPhrase: string[],
|
||||
network: 'mainnet' | 'testnet'
|
||||
): Promise<ViewKeyResult> => {
|
||||
const getViewKeyFromSeed = async (seedPhrase: string[]): Promise<ViewKeyResult> => {
|
||||
await ensureWasmInitialized()
|
||||
const seedPhraseJson = JSON.stringify(seedPhrase)
|
||||
const resultJson = get_viewkey(seedPhraseJson, network)
|
||||
const resultJson = get_viewkey(seedPhraseJson, store.getNetwork)
|
||||
return JSON.parse(resultJson)
|
||||
}
|
||||
|
||||
const importWallet = async (
|
||||
seedPhrase: string[],
|
||||
network: 'mainnet' | 'testnet' = 'testnet'
|
||||
): Promise<ViewKeyResult> => {
|
||||
const importWallet = async (seedPhrase: string[]): Promise<ViewKeyResult> => {
|
||||
try {
|
||||
store.setLoading(true)
|
||||
store.setError(null)
|
||||
@ -100,13 +90,12 @@ export function useNeptuneWallet() {
|
||||
throw new Error('Invalid seed phrase')
|
||||
}
|
||||
|
||||
const result = await getViewKeyFromSeed(seedPhrase, network)
|
||||
const result = await getViewKeyFromSeed(seedPhrase)
|
||||
|
||||
store.setSeedPhrase(seedPhrase)
|
||||
store.setReceiverId(result.receiver_identifier)
|
||||
store.setViewKey(result.view_key)
|
||||
store.setAddress(result.address)
|
||||
store.setNetwork(network)
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
@ -118,13 +107,10 @@ export function useNeptuneWallet() {
|
||||
}
|
||||
}
|
||||
|
||||
const getAddressFromSeed = async (
|
||||
seedPhrase: string[],
|
||||
network: 'mainnet' | 'testnet'
|
||||
): Promise<string> => {
|
||||
const getAddressFromSeed = async (seedPhrase: string[]): Promise<string> => {
|
||||
await ensureWasmInitialized()
|
||||
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> => {
|
||||
@ -138,10 +124,27 @@ export function useNeptuneWallet() {
|
||||
}
|
||||
}
|
||||
|
||||
const decodeViewKey = async (viewKeyHex: string): Promise<{ receiver_identifier: string }> => {
|
||||
await ensureWasmInitialized()
|
||||
const resultJson = decode_viewkey(viewKeyHex)
|
||||
return JSON.parse(resultJson)
|
||||
const decryptKeystore = async (password: string): Promise<void> => {
|
||||
try {
|
||||
const result = await (window as any).walletApi.decryptKeystore(
|
||||
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 =====
|
||||
@ -256,7 +259,7 @@ export function useNeptuneWallet() {
|
||||
store.setNetwork(network)
|
||||
|
||||
if (store.getSeedPhrase) {
|
||||
const viewKeyResult = await getViewKeyFromSeed(store.getSeedPhrase, network)
|
||||
const viewKeyResult = await getViewKeyFromSeed(store.getSeedPhrase)
|
||||
store.setAddress(viewKeyResult.address)
|
||||
store.setViewKey(viewKeyResult.view_key)
|
||||
}
|
||||
@ -268,24 +271,19 @@ export function useNeptuneWallet() {
|
||||
}
|
||||
|
||||
return {
|
||||
walletState: computed(() => store.getWallet),
|
||||
isLoading: computed(() => store.getLoading),
|
||||
error: computed(() => store.getError),
|
||||
hasWallet: computed(() => store.hasWallet),
|
||||
|
||||
initWasm: ensureWasmInitialized,
|
||||
generateWallet,
|
||||
importWallet,
|
||||
getViewKeyFromSeed,
|
||||
getAddressFromSeed,
|
||||
validateSeedPhrase,
|
||||
decodeViewKey,
|
||||
|
||||
getUtxos,
|
||||
getBalance,
|
||||
getBlockHeight,
|
||||
getNetworkInfo,
|
||||
sendTransaction,
|
||||
decryptKeystore,
|
||||
|
||||
clearWallet,
|
||||
setNetwork,
|
||||
|
||||
@ -1,19 +1,30 @@
|
||||
import * as Page from '@/views'
|
||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
export const ifAuthenticated = (to: any, from: any, next: any) => {
|
||||
const ifAuthenticated = (to: any, from: any, next: any) => {
|
||||
const neptuneStore = useNeptuneStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
if (neptuneStore.getReceiverId) {
|
||||
if (neptuneStore.hasWallet) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
authStore.setState('login')
|
||||
|
||||
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 = [
|
||||
{
|
||||
path: '/',
|
||||
@ -25,6 +36,7 @@ export const routes: any = [
|
||||
path: '/auth',
|
||||
name: 'auth',
|
||||
component: Page.Auth,
|
||||
beforeEnter: ifNotAuthenticated,
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
|
||||
@ -3,6 +3,8 @@ import { ref, computed } from 'vue'
|
||||
import type { WalletState } from '@/interface'
|
||||
|
||||
export const useNeptuneStore = defineStore('neptune', () => {
|
||||
const defaultNetwork = (import.meta.env.VITE_NODE_NETWORK || 'testnet') as 'mainnet' | 'testnet'
|
||||
|
||||
// ===== STATE =====
|
||||
const wallet = ref<WalletState>({
|
||||
seedPhrase: null,
|
||||
@ -10,11 +12,13 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
||||
receiverId: null,
|
||||
viewKey: null,
|
||||
address: null,
|
||||
network: 'testnet',
|
||||
network: defaultNetwork,
|
||||
balance: null,
|
||||
utxos: [],
|
||||
})
|
||||
|
||||
const keystorePath = ref<null | string>(null)
|
||||
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
@ -64,6 +68,10 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
||||
wallet.value = { ...wallet.value, ...walletData }
|
||||
}
|
||||
|
||||
const setKeystorePath = (path: string | null) => {
|
||||
keystorePath.value = path
|
||||
}
|
||||
|
||||
const clearWallet = () => {
|
||||
wallet.value = {
|
||||
seedPhrase: null,
|
||||
@ -71,7 +79,7 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
||||
receiverId: null,
|
||||
viewKey: null,
|
||||
address: null,
|
||||
network: 'testnet',
|
||||
network: defaultNetwork,
|
||||
balance: null,
|
||||
utxos: [],
|
||||
}
|
||||
@ -92,7 +100,7 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
||||
const hasWallet = computed(() => wallet.value.address !== null)
|
||||
const getLoading = computed(() => isLoading.value)
|
||||
const getError = computed(() => error.value)
|
||||
|
||||
const getKeystorePath = computed(() => keystorePath.value)
|
||||
return {
|
||||
getWallet,
|
||||
getSeedPhrase,
|
||||
@ -107,7 +115,7 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
||||
hasWallet,
|
||||
getLoading,
|
||||
getError,
|
||||
|
||||
getKeystorePath,
|
||||
setSeedPhrase,
|
||||
setPassword,
|
||||
setReceiverId,
|
||||
@ -119,6 +127,7 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
||||
setLoading,
|
||||
setError,
|
||||
setWallet,
|
||||
setKeystorePath,
|
||||
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
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: {
|
||||
send: (channel: string, data: any) => void;
|
||||
receive: (channel: string, func: (...args: any[]) => void) => void;
|
||||
removeAllListeners: (channel: string) => void;
|
||||
};
|
||||
}
|
||||
interface Window {
|
||||
electron: {
|
||||
send: (channel: string, data: any) => void
|
||||
receive: (channel: string, func: (...args: any[]) => void) => void
|
||||
removeAllListeners: (channel: string) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
export {}
|
||||
|
||||
@ -12,9 +12,7 @@ const handleNavigateToRecoverWallet = () => {
|
||||
|
||||
<template>
|
||||
<div class="create-tab">
|
||||
<CreateWalletComponent
|
||||
@navigate-to-recover-wallet="handleNavigateToRecoverWallet"
|
||||
/>
|
||||
<CreateWalletComponent @navigate-to-recover-wallet="handleNavigateToRecoverWallet" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@ -1,12 +1,236 @@
|
||||
<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>
|
||||
|
||||
<template>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-tab {
|
||||
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>
|
||||
|
||||
@ -31,7 +31,7 @@ const handleRecover = () => {
|
||||
Create new wallet
|
||||
</ButtonCommon>
|
||||
<ButtonCommon type="default" size="large" @click="handleRecover">
|
||||
Recover wallet
|
||||
Recover wallet
|
||||
</ButtonCommon>
|
||||
</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 = () => {
|
||||
emit('next')
|
||||
// if (isCorrect.value) {
|
||||
// correctCount.value++
|
||||
// askedPositions.value.add(quizData.value!.position)
|
||||
if (isCorrect.value) {
|
||||
correctCount.value++
|
||||
askedPositions.value.add(quizData.value!.position)
|
||||
|
||||
// if (correctCount.value >= totalQuestions) {
|
||||
// emit('next')
|
||||
// } else {
|
||||
// showResult.value = false
|
||||
// selectedAnswer.value = ''
|
||||
// const newQuiz = generateQuiz()
|
||||
// if (newQuiz) {
|
||||
// quizData.value = newQuiz
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// showResult.value = false
|
||||
// selectedAnswer.value = ''
|
||||
// const newQuiz = generateQuiz()
|
||||
// if (newQuiz) {
|
||||
// quizData.value = newQuiz
|
||||
// }
|
||||
// }
|
||||
if (correctCount.value >= totalQuestions) {
|
||||
emit('next')
|
||||
} else {
|
||||
showResult.value = false
|
||||
selectedAnswer.value = ''
|
||||
const newQuiz = generateQuiz()
|
||||
if (newQuiz) {
|
||||
quizData.value = newQuiz
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showResult.value = false
|
||||
selectedAnswer.value = ''
|
||||
const newQuiz = generateQuiz()
|
||||
if (newQuiz) {
|
||||
quizData.value = newQuiz
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
@ -216,11 +216,7 @@ onMounted(() => {
|
||||
>
|
||||
CONTINUE
|
||||
</ButtonCommon> -->
|
||||
<ButtonCommon
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleNext"
|
||||
>
|
||||
<ButtonCommon type="primary" size="large" @click="handleNext">
|
||||
CONTINUE
|
||||
</ButtonCommon>
|
||||
</div>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ButtonCommon, FormCommon } from '@/components'
|
||||
import { useNeptuneStore } from '@/stores';
|
||||
import { useNeptuneStore } from '@/stores'
|
||||
|
||||
const emit = defineEmits<{
|
||||
next: []
|
||||
navigateToOpenWallet: [event: Event]
|
||||
navigateToRecoverWallet: []
|
||||
}>()
|
||||
|
||||
const neptuneStore = useNeptuneStore()
|
||||
@ -58,13 +58,11 @@ const handleNext = () => {
|
||||
return
|
||||
}
|
||||
neptuneStore.setPassword(password.value)
|
||||
// console.log('neptuneStore.getPassword :>> ', neptuneStore.getPassword)
|
||||
emit('next')
|
||||
}
|
||||
|
||||
const handleIHaveWallet = (e: Event) => {
|
||||
e.preventDefault()
|
||||
emit('navigateToOpenWallet', e)
|
||||
const handleIHaveWallet = () => {
|
||||
emit('navigateToRecoverWallet')
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -192,9 +190,14 @@ const handleIHaveWallet = (e: Event) => {
|
||||
Create Wallet
|
||||
</ButtonCommon>
|
||||
<div class="secondary-actions">
|
||||
<button class="link-button" @click="handleIHaveWallet">
|
||||
<ButtonCommon
|
||||
type="link"
|
||||
size="small"
|
||||
class="link-button"
|
||||
@click="handleIHaveWallet"
|
||||
>
|
||||
Already have a wallet?
|
||||
</button>
|
||||
</ButtonCommon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonCommon } from '@/components'
|
||||
import { useNeptuneStore } from '@/stores/neptuneStore';
|
||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||
|
||||
const neptuneStore = useNeptuneStore()
|
||||
|
||||
@ -13,8 +13,6 @@ const handleAccessWallet = async () => {
|
||||
const seedPhrase = neptuneStore.getSeedPhraseString
|
||||
const password = neptuneStore.getPassword!
|
||||
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')
|
||||
}
|
||||
|
||||
|
||||
@ -1,29 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ButtonCommon } from '@/components'
|
||||
import { Modal } from 'ant-design-vue'
|
||||
import { ButtonCommon, PasswordForm } from '@/components'
|
||||
import { RecoverSeedComponent } from '..'
|
||||
|
||||
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 isSeedPhraseValid = ref(false)
|
||||
const showPasswordModal = ref(false)
|
||||
const seedPhrase = ref<string[]>([])
|
||||
const isLoading = ref(false)
|
||||
const passwordError = ref(false)
|
||||
|
||||
const handleSeedPhraseSubmit = (words: string[]) => {
|
||||
emit('import-success', {
|
||||
type: 'seed',
|
||||
value: words,
|
||||
})
|
||||
seedPhrase.value = words
|
||||
showPasswordModal.value = true
|
||||
}
|
||||
|
||||
const handleContinue = () => {
|
||||
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>
|
||||
<template>
|
||||
<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>
|
||||
<RecoverSeedComponent
|
||||
ref="recoverSeedComponentRef"
|
||||
@ -40,6 +62,27 @@ const handleContinue = () => {
|
||||
>Continue</ButtonCommon
|
||||
>
|
||||
</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>
|
||||
<style lang="scss" scoped>
|
||||
.import-wallet {
|
||||
|
||||
@ -5,6 +5,7 @@ import { SpinnerCommon } from '@/components'
|
||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { get_network_info } from '@neptune/wasm'
|
||||
|
||||
const neptuneStore = useNeptuneStore()
|
||||
const { getBlockHeight, getNetworkInfo } = useNeptuneWallet()
|
||||
@ -22,7 +23,6 @@ const network = computed(() => neptuneStore.getNetwork)
|
||||
const loadNetworkData = async () => {
|
||||
try {
|
||||
error.value = ''
|
||||
|
||||
const [heightResult, infoResult] = await Promise.all([getBlockHeight(), getNetworkInfo()])
|
||||
|
||||
if (heightResult !== undefined) {
|
||||
|
||||
@ -1,13 +1,31 @@
|
||||
<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 SeedPhraseDisplayComponent from '@/views/Auth/components/SeedPhraseDisplayComponent.vue'
|
||||
|
||||
const showSeedModal = ref(false)
|
||||
|
||||
const handleBackupFile = () => {
|
||||
// TODO: Implement backup file functionality
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
@ -28,6 +46,20 @@ const handleBackupSeed = () => {
|
||||
|
||||
<Divider />
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user