diff --git a/.env.example b/.env.example index 3dc32fb..b28cbae 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,2 @@ VITE_APP_API= +VITE_NODE_NETWORK= diff --git a/.gitignore b/.gitignore index 8620aeb..e8b2d7e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ dist dist-ssr coverage *.local -.vite/build/* +.vite/*/** wallets/* /cypress/videos/ diff --git a/electron/ipcHandlers.ts b/electron/ipcHandlers.ts index 700c06d..a137f07 100644 --- a/electron/ipcHandlers.ts +++ b/electron/ipcHandlers.ts @@ -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) } + } +}) diff --git a/electron/preload.ts b/electron/preload.ts index 7c35ce6..1ffceeb 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -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'), }) diff --git a/src/components/WalletInfo.vue b/src/components/WalletInfo.vue index 9b160bf..1059f58 100644 --- a/src/components/WalletInfo.vue +++ b/src/components/WalletInfo.vue @@ -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:
-
+
{{ receiveAddress || 'No address available' }} {
+
@@ -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; } } } diff --git a/src/components/common/FormCommon.vue b/src/components/common/FormCommon.vue index 8228a5d..e11da64 100644 --- a/src/components/common/FormCommon.vue +++ b/src/components/common/FormCommon.vue @@ -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" /> - +
{{ error }}
diff --git a/src/components/common/PasswordForm.vue b/src/components/common/PasswordForm.vue new file mode 100644 index 0000000..01cefee --- /dev/null +++ b/src/components/common/PasswordForm.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/src/components/index.ts b/src/components/index.ts index 7aa6125..8eb46d8 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -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 } diff --git a/src/composables/useNeptuneWallet.ts b/src/composables/useNeptuneWallet.ts index 3ecd638..7f85a99 100644 --- a/src/composables/useNeptuneWallet.ts +++ b/src/composables/useNeptuneWallet.ts @@ -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 => { + const getViewKeyFromSeed = async (seedPhrase: string[]): Promise => { 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 => { + const importWallet = async (seedPhrase: string[]): Promise => { 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 => { + const getAddressFromSeed = async (seedPhrase: string[]): Promise => { 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 => { @@ -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 => { + 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, diff --git a/src/router/route.ts b/src/router/route.ts index f628f20..73575d7 100644 --- a/src/router/route.ts +++ b/src/router/route.ts @@ -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(.*)*', diff --git a/src/stores/neptuneStore.ts b/src/stores/neptuneStore.ts index 6389f66..e78f477 100644 --- a/src/stores/neptuneStore.ts +++ b/src/stores/neptuneStore.ts @@ -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({ 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) + const isLoading = ref(false) const error = ref(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, } }) diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index aab3691..1ea0b48 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -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 {} diff --git a/src/views/Auth/components/CreateTab.vue b/src/views/Auth/components/CreateTab.vue index 5f951b4..b3b451f 100644 --- a/src/views/Auth/components/CreateTab.vue +++ b/src/views/Auth/components/CreateTab.vue @@ -12,9 +12,7 @@ const handleNavigateToRecoverWallet = () => { diff --git a/src/views/Auth/components/LoginTab.vue b/src/views/Auth/components/LoginTab.vue index 2ca953a..645b793 100644 --- a/src/views/Auth/components/LoginTab.vue +++ b/src/views/Auth/components/LoginTab.vue @@ -1,12 +1,236 @@ + + diff --git a/src/views/Auth/components/OnboardingTab.vue b/src/views/Auth/components/OnboardingTab.vue index 1e05fe7..796228e 100644 --- a/src/views/Auth/components/OnboardingTab.vue +++ b/src/views/Auth/components/OnboardingTab.vue @@ -31,7 +31,7 @@ const handleRecover = () => { Create new wallet - Recover wallet + Recover wallet
Thank you for being a part of the Neptune community!
diff --git a/src/views/Auth/components/create/ConfirmSeedComponent.vue b/src/views/Auth/components/create/ConfirmSeedComponent.vue index 3119114..736156e 100644 --- a/src/views/Auth/components/create/ConfirmSeedComponent.vue +++ b/src/views/Auth/components/create/ConfirmSeedComponent.vue @@ -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 --> - + CONTINUE diff --git a/src/views/Auth/components/create/CreatePasswordStep.vue b/src/views/Auth/components/create/CreatePasswordStep.vue index c7c633b..69c4898 100644 --- a/src/views/Auth/components/create/CreatePasswordStep.vue +++ b/src/views/Auth/components/create/CreatePasswordStep.vue @@ -1,11 +1,11 @@ @@ -192,9 +190,14 @@ const handleIHaveWallet = (e: Event) => { Create Wallet
- +
diff --git a/src/views/Auth/components/create/WalletCreatedStep.vue b/src/views/Auth/components/create/WalletCreatedStep.vue index 7aa86b1..04cded3 100644 --- a/src/views/Auth/components/create/WalletCreatedStep.vue +++ b/src/views/Auth/components/create/WalletCreatedStep.vue @@ -1,6 +1,6 @@