Compare commits

..

No commits in common. "065b77db84861a95d7c5f54a27604214e34981af" and "3040bd5a57b9d415625164d9dc8946c653c3e5f3" have entirely different histories.

39 changed files with 1193 additions and 1972 deletions

View File

@ -3,9 +3,6 @@
"version": "0.0.0",
"private": true,
"type": "module",
"workspaces": [
"packages/*"
],
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
@ -16,7 +13,6 @@
"format": "prettier --write src/"
},
"dependencies": {
"@neptune/wasm": "file:./packages/neptune-wasm",
"ant-design-vue": "^4.2.6",
"axios": "^1.6.8",
"dayjs": "^1.11.10",

View File

@ -1,22 +1,41 @@
import axios from 'axios'
import router from '@/router'
import { STATUS_CODE_SUCCESS, ACCESS_TOKEN, STATUS_CODE_UNAUTHORIZED } from '@/utils'
axios.defaults.withCredentials = false
export const API_URL = import.meta.env.VITE_APP_API
const instance = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
})
instance.interceptors.request.use(
function (config: any) {
try {
const token = localStorage.getItem(ACCESS_TOKEN)
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
} catch (error) {
throw Error('')
}
return config
},
function (error) {
return Promise.reject(error)
}
)
instance.interceptors.response.use(
function (response) {
// if (response?.status !== STATUS_CODE_SUCCESS) return Promise.reject(response?.data)
return response
if (response?.status !== STATUS_CODE_SUCCESS) return Promise.reject(response?.data)
return response.data
},
function (error) {
if (error?.response?.status === STATUS_CODE_UNAUTHORIZED || error.code === 'ERR_NETWORK') {
localStorage.clear()
return router.push({ name: 'login' })
}
if (error?.response?.data) {
return Promise.reject(error?.response?.data)
}

View File

@ -1,53 +0,0 @@
import { callJsonRpc } from '@/api/request'
export const getUtxosFromViewKey = (
viewKey: string,
startBlock: number = 0,
endBlock: number | null = null,
maxSearchDepth: number = 1000
) => {
const params = {
viewKey,
startBlock,
endBlock,
maxSearchDepth,
}
return callJsonRpc('wallet_getUtxosFromViewKey', params)
}
export const getBalance = async (): Promise<any> => {
return await callJsonRpc('wallet_balance', [])
}
export const getBlockHeight = async (): Promise<any> => {
return await callJsonRpc('chain_height', [])
}
export const getNetworkInfo = async (): Promise<any> => {
return await callJsonRpc('node_network', [])
}
export const getWalletAddress = async (): Promise<any> => {
return await callJsonRpc('wallet_address', [])
}
export const sendTransaction = async (
toAddress: string,
amount: string,
fee: string
): Promise<any> => {
const params = [toAddress, amount, fee]
return await callJsonRpc('wallet_send', params)
}
export const broadcastSignedTransaction = async (signedTxData: any): Promise<any> => {
return await callJsonRpc('wallet_broadcastSignedTransaction', signedTxData)
}
export const getTransaction = async (txHash: string): Promise<any> => {
return await callJsonRpc('chain_transaction', [txHash])
}
export const getMempoolInfo = async (): Promise<any> => {
return await callJsonRpc('chain_mempool', [])
}

View File

@ -1,14 +0,0 @@
import request from '@/api/config'
export const callJsonRpc = (method: string, params: any = []) => {
const requestData = {
jsonrpc: '2.0',
method,
params,
id: 1,
}
return request({
method: 'POST',
data: requestData,
})
}

View File

@ -1,108 +1,32 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref } from 'vue'
import ButtonCommon from '@/components/common/ButtonCommon.vue'
import { formatNumberToLocaleString } from '@/utils'
import { useNeptuneStore } from '@/stores/neptuneStore'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { message } from 'ant-design-vue'
import { ButtonCommon, SpinnerCommon } from '@/components'
const neptuneStore = useNeptuneStore()
const { getBalance, getBlockHeight } = useNeptuneWallet()
const availableBalance = ref(0)
const pendingBalance = ref(0)
const currentDaaScore = ref(0)
const isLoadingData = ref(false)
const receiveAddress = ref('kaspa:qpn80v050r3jxv6mzt8tzss6dhvllc3rvcuuy86z6djgmvzx0napvhuj7ugh9')
const walletStatus = ref('Online')
const currentDaaScore = ref(255953336)
const receiveAddress = computed(() => neptuneStore.getWallet?.address || '')
const walletStatus = computed(() => {
if (neptuneStore.getLoading) return 'Loading...'
if (neptuneStore.getError) return 'Error'
if (neptuneStore.getWallet?.address) return 'Online'
return 'Offline'
})
const copyAddress = async () => {
if (!receiveAddress.value) {
message.error('No address available')
return
}
try {
await navigator.clipboard.writeText(receiveAddress.value)
message.success('Address copied to clipboard!')
} catch (err) {
message.error('Failed to copy address')
}
const copyAddress = () => {
navigator.clipboard.writeText(receiveAddress.value)
}
const handleSend = () => {
// TODO: Implement send transaction functionality
console.log('Send clicked')
}
const loadWalletData = async () => {
if (!receiveAddress.value) return
isLoadingData.value = true
try {
const [balanceResult, blockHeightResult] = await Promise.all([
getBalance(),
getBlockHeight(),
])
if (balanceResult) {
if (typeof balanceResult === 'number') {
availableBalance.value = balanceResult
} else if (balanceResult.confirmed !== undefined) {
availableBalance.value = balanceResult.confirmed || 0
pendingBalance.value = balanceResult.unconfirmed || 0
} else if (balanceResult.balance !== undefined) {
availableBalance.value = parseFloat(balanceResult.balance) || 0
}
}
if (blockHeightResult) {
currentDaaScore.value =
typeof blockHeightResult === 'number'
? blockHeightResult
: blockHeightResult.height || 0
}
} catch (error) {
message.error('Failed to load wallet data')
} finally {
isLoadingData.value = false
}
}
onMounted(() => {
loadWalletData()
})
</script>
<template>
<div class="wallet-info-container">
<div v-if="isLoadingData && !receiveAddress" class="loading-state">
<SpinnerCommon size="medium" />
<p>Loading wallet data...</p>
</div>
<div v-else-if="!receiveAddress" class="empty-state">
<p>No wallet found. Please create or import a wallet.</p>
</div>
<template v-else>
<!-- Balance Section -->
<div class="balance-section">
<div class="balance-label">Available</div>
<div class="balance-amount">
<span v-if="isLoadingData">Loading...</span>
<span v-else>{{ formatNumberToLocaleString(availableBalance) }} NEPT</span>
</div>
<div class="balance-amount">{{ availableBalance }} KAS</div>
<div class="pending-section">
<span class="pending-label">Pending</span>
<span class="pending-amount">
{{ isLoadingData ? '...' : formatNumberToLocaleString(pendingBalance) }}
NEPT
</span>
<span class="pending-amount">{{ pendingBalance }} KAS</span>
</div>
</div>
@ -110,7 +34,7 @@ onMounted(() => {
<div class="receive-section">
<div class="address-label">Receive Address:</div>
<div class="address-value" @click="copyAddress">
{{ receiveAddress || 'No address available' }}
{{ receiveAddress }}
<svg
class="copy-icon"
xmlns="http://www.w3.org/2000/svg"
@ -126,13 +50,7 @@ onMounted(() => {
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<ButtonCommon
type="primary"
size="large"
block
@click="handleSend"
class="btn-send"
>
<ButtonCommon type="primary" size="large" block @click="handleSend" class="btn-send">
SEND
</ButtonCommon>
</div>
@ -143,13 +61,9 @@ onMounted(() => {
>Wallet Status: <strong>{{ walletStatus }}</strong></span
>
<span
>DAA Score:
<strong>{{
isLoadingData ? '...' : formatNumberToLocaleString(currentDaaScore)
}}</strong></span
>DAA score: <strong>{{ formatNumberToLocaleString(currentDaaScore) }}</strong></span
>
</div>
</template>
</div>
</template>
@ -158,18 +72,6 @@ onMounted(() => {
@include card-base;
}
.loading-state,
.empty-state {
text-align: center;
padding: var(--spacing-3xl);
color: var(--text-secondary);
p {
margin: var(--spacing-lg) 0 0;
font-size: var(--font-base);
}
}
.balance-section {
text-align: center;
margin-bottom: var(--spacing-3xl);

View File

@ -1,62 +1,141 @@
<script setup lang="ts">
import { ref, defineEmits, onMounted, computed } from 'vue'
import { ref, defineEmits, onMounted } from 'vue'
import { ButtonCommon } from '@/components'
import { useNeptuneStore } from '@/stores/neptuneStore'
import { message } from 'ant-design-vue'
import { useSeedStore } from '@/stores'
const emit = defineEmits<{
next: []
back: []
}>()
const neptuneStore = useNeptuneStore()
const seedStore = useSeedStore()
const seedWords = computed(() => neptuneStore.getSeedPhrase || [])
const seedWords = ref<string[]>([])
const currentQuestionIndex = ref(0)
const selectedAnswer = ref('')
const isCorrect = ref(false)
const showResult = ref(false)
const correctCount = ref(0)
const totalQuestions = 3
const askedPositions = ref<Set<number>>(new Set())
const generateQuiz = (): {
position: number
correctWord: string
options: string[]
} | null => {
if (!seedWords.value || seedWords.value.length === 0) {
message.error('No seed phrase found')
return null
}
let randomPosition: number
let attempts = 0
const maxAttempts = 50
do {
randomPosition = Math.floor(Math.random() * seedWords.value.length) + 1
attempts++
if (attempts > maxAttempts) {
message.error('Unable to generate new question')
return null
}
} while (askedPositions.value.has(randomPosition))
if (seedWords.value.length === 0) return null
const randomPosition = Math.floor(Math.random() * 12) + 1
currentQuestionIndex.value = randomPosition - 1
const correctWord = seedWords.value[randomPosition - 1]
const options = [correctWord]
const otherWords = seedWords.value.filter((_, index) => index !== randomPosition - 1)
while (options.length < 4 && otherWords.length > 0) {
const randomIndex = Math.floor(Math.random() * otherWords.length)
const randomWord = otherWords[randomIndex]
const BIP39_WORDS = [
'abandon',
'ability',
'able',
'about',
'above',
'absent',
'absorb',
'abstract',
'absurd',
'abuse',
'access',
'accident',
'account',
'accuse',
'achieve',
'acid',
'acoustic',
'acquire',
'across',
'act',
'action',
'actor',
'actress',
'actual',
'adapt',
'add',
'addict',
'address',
'adjust',
'admit',
'adult',
'advance',
'advice',
'aerobic',
'affair',
'afford',
'afraid',
'again',
'age',
'agent',
'agree',
'ahead',
'aim',
'air',
'airport',
'aisle',
'alarm',
'album',
'alcohol',
'alert',
'alien',
'all',
'alley',
'allow',
'almost',
'alone',
'alpha',
'already',
'also',
'alter',
'always',
'amateur',
'amazing',
'among',
'amount',
'amused',
'analyst',
'anchor',
'ancient',
'anger',
'angle',
'angry',
'animal',
'ankle',
'announce',
'annual',
'another',
'answer',
'antenna',
'antique',
'anxiety',
'any',
'apart',
'apology',
'appear',
'apple',
'approve',
'april',
'arch',
'arctic',
'area',
'arena',
'argue',
'arm',
'armed',
'armor',
'army',
'around',
'arrange',
'arrest',
]
while (options.length < 4) {
const randomWord = BIP39_WORDS[Math.floor(Math.random() * BIP39_WORDS.length)]
if (!options.includes(randomWord)) {
options.push(randomWord)
otherWords.splice(randomIndex, 1)
}
}
@ -83,10 +162,6 @@ const handleAnswerSelect = (answer: string) => {
const handleNext = () => {
if (isCorrect.value) {
correctCount.value++
askedPositions.value.add(quizData.value!.position)
if (correctCount.value >= totalQuestions) {
emit('next')
} else {
showResult.value = false
@ -96,32 +171,37 @@ const handleNext = () => {
quizData.value = newQuiz
}
}
} else {
showResult.value = false
selectedAnswer.value = ''
const newQuiz = generateQuiz()
if (newQuiz) {
quizData.value = newQuiz
}
}
}
const handleBack = () => {
emit('back')
}
onMounted(() => {
const seeds = neptuneStore.getSeedPhrase
if (!seeds || seeds.length === 0) {
message.warning('No seed phrase found. Please go back and generate a wallet first.')
return
}
const words = seedStore.getSeedWords()
if (words.length > 0) {
seedWords.value = words
const newQuiz = generateQuiz()
if (newQuiz) {
quizData.value = newQuiz
}
} else {
const sampleWords = [
'abandon',
'ability',
'able',
'about',
'above',
'absent',
'absorb',
'abstract',
'absurd',
'abuse',
'access',
'accident',
]
seedWords.value = sampleWords
const newQuiz = generateQuiz()
if (newQuiz) {
quizData.value = newQuiz
}
}
})
</script>
@ -144,9 +224,6 @@ onMounted(() => {
Make sure you wrote the phrase down correctly by answering this quick
checkup.
</p>
<p class="progress-text">
Question {{ correctCount + 1 }} / {{ totalQuestions }}
</p>
</div>
<div class="quiz-section">
@ -173,42 +250,14 @@ onMounted(() => {
</div>
<div v-if="showResult" class="result-message">
<p
v-if="isCorrect && correctCount + 1 >= totalQuestions"
class="success-message"
>
Correct! You answered all {{ totalQuestions }} questions correctly.
</p>
<p v-else-if="isCorrect" class="success-message">
Correct! Next question...
</p>
<p v-if="isCorrect" class="success-message"> Correct! You can proceed.</p>
<p v-else class="error-message"> Incorrect. Please try again.</p>
</div>
</div>
<div class="confirm-actions">
<ButtonCommon
v-if="
!showResult ||
!isCorrect ||
(isCorrect && correctCount + 1 < totalQuestions)
"
type="default"
size="large"
@click="handleBack"
>
BACK
</ButtonCommon>
<ButtonCommon
v-if="showResult && isCorrect && correctCount + 1 < totalQuestions"
type="primary"
size="large"
@click="handleNext"
>
NEXT QUESTION
</ButtonCommon>
<ButtonCommon
v-if="showResult && isCorrect && correctCount + 1 >= totalQuestions"
v-if="showResult && isCorrect"
type="primary"
size="large"
@click="handleNext"
@ -282,13 +331,6 @@ onMounted(() => {
line-height: var(--leading-normal);
margin: 0;
}
.progress-text {
margin-top: var(--spacing-md);
font-weight: var(--font-bold);
color: var(--primary-color);
font-size: var(--font-base);
}
}
.quiz-section {
@ -369,14 +411,10 @@ onMounted(() => {
display: flex;
justify-content: space-between;
gap: var(--spacing-md);
&:has(:only-child) {
justify-content: flex-end;
}
}
}
@include screen(mobile) {
@media (max-width: 640px) {
.confirm-container {
padding: var(--spacing-md);
}
@ -385,7 +423,6 @@ onMounted(() => {
max-width: 100%;
}
.confirm-content {
.quiz-section {
.answer-options {
grid-template-columns: 1fr;
@ -395,6 +432,5 @@ onMounted(() => {
.confirm-actions {
flex-direction: column;
}
}
}
</style>

View File

@ -0,0 +1,561 @@
<script setup lang="ts">
import { ref, computed, defineEmits } from 'vue'
import { ButtonCommon, FormCommon, KeystoreDownloadComponent } from '@/components'
const emit = defineEmits<{
navigateToOpenWallet: [event: Event]
navigateToRecoverySeed: []
}>()
const step = ref(1)
const password = ref('')
const confirmPassword = ref('')
const passwordError = ref('')
const confirmPasswordError = ref('')
const passwordStrength = computed(() => {
if (!password.value) return { level: 0, text: '', color: '' }
let strength = 0
const checks = {
length: password.value.length >= 8,
uppercase: /[A-Z]/.test(password.value),
lowercase: /[a-z]/.test(password.value),
number: /[0-9]/.test(password.value),
special: /[!@#$%^&*(),.?":{}|<>]/.test(password.value),
}
strength = Object.values(checks).filter(Boolean).length
if (strength <= 2) return { level: 1, text: 'Weak', color: 'var(--error-color)' }
if (strength <= 3) return { level: 2, text: 'Medium', color: 'var(--warning-color)' }
if (strength <= 4) return { level: 3, text: 'Good', color: 'var(--info-color)' }
return { level: 4, text: 'Strong', color: 'var(--success-color)' }
})
const isPasswordMatch = computed(() => {
if (!confirmPassword.value) return true
return password.value === confirmPassword.value
})
const canProceed = computed(() => {
return (
password.value.length >= 8 &&
confirmPassword.value.length >= 8 &&
isPasswordMatch.value &&
passwordStrength.value.level >= 2
)
})
const handleIHaveWallet = (e: Event) => {
e.preventDefault()
emit('navigateToOpenWallet', e)
}
const handleNextPassword = () => {
if (!canProceed.value) {
if (password.value.length < 8) {
passwordError.value = 'Password must be at least 8 characters'
}
if (!isPasswordMatch.value) {
confirmPasswordError.value = 'Passwords do not match'
}
return
}
step.value = 2
}
function downloadKeystoreFile() {
// Gi lp ni dung keystore (có th tu chnh)
const data = {
account: 'kaspa-wallet',
version: 1,
enc: 'mock-data',
created: new Date().toISOString(),
note: 'Exported from web-wallet',
hint: 'Replace bằng file thực tế trong tích hợp thật.',
}
const file = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const link = document.createElement('a')
link.href = URL.createObjectURL(file)
link.download = 'kaspa-wallet-keystore.json'
link.click()
setTimeout(() => URL.revokeObjectURL(link.href), 2300)
step.value = 3
}
function handleBack() {
if (step.value === 2) step.value = 1
else if (step.value === 3) step.value = 2
}
function resetAll() {
password.value = ''
confirmPassword.value = ''
passwordError.value = ''
confirmPasswordError.value = ''
step.value = 1
}
</script>
<template>
<div class="auth-container">
<div class="auth-card">
<template v-if="step === 1">
<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="neptuneGradient"
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="ringGradient"
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(#neptuneGradient)" />
<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(#ringGradient)"
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">NPTUN</span>
</div>
</div>
<h1 class="auth-title">Create New Wallet</h1>
<p class="auth-subtitle">Secure your wallet with a strong password</p>
</div>
<div class="auth-card-content">
<div class="form-group">
<FormCommon
v-model="password"
type="password"
label="Create Password"
placeholder="Enter your password"
show-password-toggle
required
:error="passwordError"
@input="passwordError = ''"
/>
<div v-if="password" class="password-strength">
<div class="strength-bar">
<div
class="strength-fill"
:style="{
width: `${(passwordStrength.level / 4) * 100}%`,
backgroundColor: passwordStrength.color,
}"
></div>
</div>
<span
class="strength-text"
:style="{ color: passwordStrength.color }"
>{{ passwordStrength.text }}</span
>
</div>
</div>
<div class="form-group">
<FormCommon
v-model="confirmPassword"
type="password"
label="Confirm Password"
placeholder="Re-enter your password"
show-password-toggle
required
:error="confirmPasswordError"
@input="confirmPasswordError = ''"
/>
<div
v-if="confirmPassword"
class="password-match"
:class="{ match: isPasswordMatch }"
>
<span v-if="isPasswordMatch" class="match-text">
Passwords match
</span>
<span v-else class="match-text error"> Passwords do not match </span>
</div>
</div>
<p class="helper-text">
Password must be at least 8 characters with uppercase, lowercase, and
numbers.
</p>
<div class="auth-button-group">
<ButtonCommon
type="primary"
size="large"
class="auth-button"
block
:disabled="!canProceed"
@click="handleNextPassword"
>Create Wallet</ButtonCommon
>
<div class="secondary-actions">
<button class="link-button" @click="handleIHaveWallet">
Already have a wallet?
</button>
</div>
</div>
</div>
</template>
<template v-else-if="step === 2">
<KeystoreDownloadComponent @download="downloadKeystoreFile" @back="handleBack" />
</template>
<template v-else-if="step === 3">
<div class="well-done-step">
<h2 class="done-main">You are done!</h2>
<p class="done-desc">
You are now ready to take advantage of all that your wallet has to offer!
Access with keystore file should only be used in an offline setting.
</p>
<div class="center-svg" style="margin: 14px auto 12px auto">
<svg width="180" height="95" viewBox="0 0 175 92" fill="none">
<rect x="111" y="37" width="64" height="33" rx="7" fill="#23B1EC" />
<rect
x="30.5"
y="37.5"
width="80"
height="46"
rx="7.5"
fill="#D6F9FE"
stroke="#AEEBF8"
stroke-width="5"
/>
<rect x="56" y="67" width="32" height="10" rx="3" fill="#B0F3A6" />
<rect x="46" y="49" width="52" height="12" rx="3" fill="#a2d2f5" />
<circle cx="155" cy="52" r="8" fill="#fff" />
<rect x="121" y="43" width="27" height="7" rx="1.5" fill="#5AE9D2" />
<rect x="128" y="59" width="17" height="4" rx="1.5" fill="#FCEBBA" />
<circle cx="40" cy="27" r="7" fill="#A2D2F5" />
<g>
<circle cx="128" cy="21" r="3" fill="#FF8585" />
<circle cx="57.5" cy="20.5" r="1.5" fill="#67DEFF" />
<rect x="95" y="18" width="7" height="5" rx="2" fill="#A2D2F5" />
</g>
</svg>
</div>
<div class="btn-row">
<ButtonCommon
class="done-btn"
type="primary"
size="large"
block
style="margin-bottom: 0.3em"
@click="$router.push('/')"
>Access Wallet</ButtonCommon
>
<button class="done-link" type="button" @click="resetAll">
Create Another Wallet
</button>
</div>
</div>
</template>
<template v-else>
<slot> </slot>
</template>
</div>
</div>
</template>
<style lang="scss" scoped>
.auth-container {
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-xl);
background: var(--bg-light);
}
.auth-card {
@include card-base;
max-width: 720px;
width: 100%;
@media (max-width: 640px) {
max-width: 100%;
}
}
.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;
}
}
.auth-card-content {
.form-group {
margin-bottom: var(--spacing-xl);
}
}
.password-strength {
margin-top: var(--spacing-sm);
display: flex;
align-items: center;
gap: var(--spacing-md);
.strength-bar {
flex: 1;
height: 4px;
background: var(--border-light);
border-radius: var(--radius-full);
overflow: hidden;
.strength-fill {
height: 100%;
transition: all 0.3s ease;
}
}
.strength-text {
font-size: var(--font-xs);
font-weight: var(--font-medium);
min-width: 50px;
text-align: right;
}
}
.password-match {
margin-top: var(--spacing-sm);
font-size: var(--font-xs);
&.match .match-text {
color: var(--success-color);
}
.match-text.error {
color: var(--error-color);
}
}
.helper-text {
font-size: var(--font-xs);
color: var(--text-muted);
margin: 0 0 var(--spacing-xl);
line-height: var(--leading-normal);
}
.auth-button {
width: fit-content;
margin: 0 auto;
}
.auth-button-group {
margin-top: var(--spacing-2xl);
display: flex;
flex-direction: column;
.secondary-actions {
display: flex;
justify-content: center;
align-items: center;
gap: var(--spacing-sm);
margin-top: var(--spacing-md);
}
.link-button {
background: none;
border: none;
color: var(--primary-color);
font-size: var(--font-sm);
cursor: pointer;
transition: color 0.2s ease;
padding: 0;
&:hover {
color: var(--primary-hover);
text-decoration: underline;
}
}
.separator {
color: var(--text-muted);
font-size: var(--font-sm);
}
}
@media (max-width: 640px) {
.auth-container {
padding: var(--spacing-md);
}
.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);
}
}
}
.well-done-step {
text-align: center;
padding: 20px 8px;
.done-title {
color: var(--primary-color);
font-weight: 700;
letter-spacing: 0.07em;
margin-bottom: 1px;
}
.done-main {
font-size: 1.36rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 2px;
}
.done-desc {
color: var(--text-secondary);
font-size: 1.11em;
max-width: 410px;
margin: 2px auto 15px auto;
}
.center-svg {
display: flex;
justify-content: center;
}
.btn-row {
display: flex;
flex-direction: column;
gap: 11px;
align-items: center;
width: 100%;
max-width: 400px;
margin: 0 auto 5px auto;
}
.done-btn {
margin-bottom: 0.3em;
}
.done-link {
background: none;
border: none;
color: var(--primary-color);
font-size: 1em;
text-decoration: underline;
cursor: pointer;
margin: 0 auto;
font-weight: 600;
}
}
</style>

View File

@ -6,16 +6,16 @@ import { validateSeedPhrase18 } from '@/utils/helpers/seedPhrase'
const emit = defineEmits<{
(
e: 'import-success',
data: { type: 'seed' | 'keystore'; value: string | string[]; passphrase?: string }
data: { type: 'seed' | 'privatekey'; value: string | string[]; passphrase?: string }
): void
}>()
const tab = ref<'seedphrase' | 'keystore'>('seedphrase')
const tab = ref<'seedphrase' | 'privatekey'>('seedphrase')
const seedWords = ref<string[]>(Array(18).fill(''))
const seedError = ref('')
const passphrase = ref('')
const keystore = ref('')
const keystoreError = ref('')
const privateKey = ref('')
const privateKeyError = ref('')
const inputBoxFocus = (idx: number) => {
document.getElementById('input-' + idx)?.focus()
@ -34,12 +34,12 @@ const validateSeed = () => {
return true
}
const validateKeystore = () => {
if (!keystore.value.trim()) {
keystoreError.value = 'Please enter your keystore.'
const validateKey = () => {
if (!privateKey.value.trim()) {
privateKeyError.value = 'Please enter your private key.'
return false
}
keystoreError.value = ''
privateKeyError.value = ''
return true
}
@ -53,8 +53,8 @@ const handleContinue = () => {
})
}
} else {
if (validateKeystore()) {
emit('import-success', { type: 'keystore', value: keystore.value })
if (validateKey()) {
emit('import-success', { type: 'privatekey', value: privateKey.value })
}
}
}
@ -70,8 +70,11 @@ const handleContinue = () => {
>
Import by seed phrase
</button>
<button :class="['tab-btn', tab === 'keystore' && 'active']" @click="tab = 'keystore'">
Import by keystore
<button
:class="['tab-btn', tab === 'privatekey' && 'active']"
@click="tab = 'privatekey'"
>
Import by private key
</button>
</div>
<div v-if="tab === 'seedphrase'" class="tab-pane">
@ -110,12 +113,12 @@ const handleContinue = () => {
<div v-else class="tab-pane">
<div class="form-row mb-md">
<FormCommon
v-model="keystore"
v-model="privateKey"
type="text"
label="Keystore"
placeholder="Enter keystore"
:error="keystoreError"
@focus="keystoreError = ''"
label="Private key"
placeholder="Enter private key"
:error="privateKeyError"
@focus="privateKeyError = ''"
/>
</div>
</div>
@ -127,7 +130,7 @@ const handleContinue = () => {
:disabled="
tab === 'seedphrase'
? !seedWords.every((w) => w) || !!seedError
: !keystore || !!keystoreError
: !privateKey || !!privateKeyError
"
@click="handleContinue"
>Continue</ButtonCommon
@ -255,7 +258,7 @@ const handleContinue = () => {
margin-top: 3px;
}
}
@include screen(mobile) {
@media (max-width: 600px) {
.import-wallet {
padding: 16px 5px;
}

View File

@ -218,19 +218,13 @@ function handleBack() {
margin-right: 8px;
}
}
@include screen(tablet) {
@media (max-width: 900px) {
.steps-bar .step {
min-width: 90px;
font-size: 14px;
}
}
@include screen(mobile) {
.steps-bar .step {
min-width: 90px;
font-size: 14px;
}
@media (max-width: 650px) {
.keystore-step {
padding: 15px 3px 13px 3px;
}

View File

@ -188,7 +188,7 @@ const navigateToNewWallet = () => {
max-width: 420px;
width: 100%;
@include screen(mobile) {
@media (max-width: 640px) {
max-width: 100%;
}
}
@ -312,11 +312,12 @@ const navigateToNewWallet = () => {
}
// Responsive Design
@include screen(mobile) {
@media (max-width: 640px) {
.auth-container {
padding: var(--spacing-md);
}
.auth-card-header {
.logo-container {
.logo-circle {
width: 40px;
@ -330,6 +331,11 @@ const navigateToNewWallet = () => {
}
}
.auth-title {
font-size: var(--font-xl);
}
}
.wallet-icon {
.icon-circle {
width: 64px;

View File

@ -1,44 +1,31 @@
<script setup lang="ts">
import { defineEmits, computed } from 'vue'
import { ref, defineEmits, onMounted } from 'vue'
import { ButtonCommon } from '@/components'
import { useNeptuneStore } from '@/stores/neptuneStore'
import { message } from 'ant-design-vue'
import { generateSeedPhrase } from '@/utils'
import { useSeedStore } from '@/stores'
const emit = defineEmits<{
next: []
back: []
}>()
const neptuneStore = useNeptuneStore()
const seedStore = useSeedStore()
const seedWords = computed(() => neptuneStore.getSeedPhrase || [])
const seedWords = ref<string[]>([])
onMounted(() => {
const words = generateSeedPhrase()
seedWords.value = words
seedStore.setSeedWords(words)
})
const handleNext = () => {
if (!seedWords.value || seedWords.value.length === 0) {
message.error('❌ Cannot proceed without seed phrase')
return
}
emit('next')
}
const handleBack = () => {
emit('back')
}
const handleCopySeed = async () => {
if (!seedWords.value || seedWords.value.length === 0) {
message.error('No seed phrase to copy')
return
}
try {
const seedPhrase = seedWords.value.join(' ')
await navigator.clipboard.writeText(seedPhrase)
message.success('Seed phrase copied to clipboard!')
} catch (err) {
message.error('Failed to copy seed phrase')
}
}
</script>
<template>
@ -52,49 +39,27 @@ const handleCopySeed = async () => {
<div class="instruction-text">
<p>
Your wallet is accessible by a seed phrase. The seed phrase is an ordered
18-word secret phrase.
12-word secret phrase.
</p>
<p>
Make sure no one is looking, as anyone with your seed phrase can access your
wallet and funds. Write it down and keep it safe.
wallet your funds. Write it down and keep it safe.
</p>
</div>
<div v-if="seedWords.length > 0" class="seed-words-container">
<div class="seed-words-container">
<div class="seed-words-grid">
<div v-for="(word, index) in seedWords" :key="index" class="seed-word-item">
<span class="word-number">{{ index + 1 }}</span>
<span class="word-text">{{ word }}</span>
</div>
</div>
<button class="copy-seed-btn" @click="handleCopySeed" type="button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path
d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
></path>
</svg>
Copy Seed Phrase
</button>
</div>
<div v-else class="no-seed-warning">
<p> No seed phrase found. Please go back and generate a wallet first.</p>
</div>
<div class="cool-fact">
<p>
Cool fact: there are more 18-word phrase combinations than atoms in the
observable universe!
Cool fact: there are more 12-word phrase combinations than nanoseconds since
the big bang!
</p>
</div>
@ -102,12 +67,7 @@ const handleCopySeed = async () => {
<ButtonCommon type="default" size="large" @click="handleBack">
BACK
</ButtonCommon>
<ButtonCommon
type="primary"
size="large"
@click="handleNext"
:disabled="!seedWords || seedWords.length === 0"
>
<ButtonCommon type="primary" size="large" @click="handleNext">
NEXT
</ButtonCommon>
</div>
@ -174,7 +134,6 @@ const handleCopySeed = async () => {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--spacing-md);
margin-bottom: var(--spacing-md);
.seed-word-item {
display: flex;
@ -199,37 +158,6 @@ const handleCopySeed = async () => {
}
}
}
.copy-seed-btn {
display: flex;
align-items: center;
gap: var(--spacing-sm);
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--primary-color);
color: var(--text-light);
border: none;
border-radius: var(--radius-sm);
font-size: var(--font-sm);
font-weight: var(--font-medium);
cursor: pointer;
transition: all 0.2s ease;
justify-content: center;
svg {
flex-shrink: 0;
}
&:hover {
background: var(--primary-hover);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 127, 207, 0.3);
}
&:active {
transform: translateY(0);
}
}
}
.cool-fact {
@ -249,25 +177,10 @@ const handleCopySeed = async () => {
justify-content: space-between;
gap: var(--spacing-md);
}
.no-seed-warning {
padding: var(--spacing-xl);
background: var(--error-light);
border: 2px solid var(--error-color);
border-radius: var(--radius-md);
text-align: center;
margin-bottom: var(--spacing-2xl);
p {
font-size: var(--font-md);
color: var(--error-color);
font-weight: var(--font-bold);
margin: 0;
}
}
}
@include screen(mobile) {
// Responsive Design
@media (max-width: 640px) {
.recovery-container {
padding: var(--spacing-md);
}
@ -276,7 +189,6 @@ const handleCopySeed = async () => {
max-width: 100%;
}
.recovery-content {
.seed-words-container {
.seed-words-grid {
grid-template-columns: repeat(2, 1fr);
@ -291,6 +203,5 @@ const handleCopySeed = async () => {
}
}
}
}
}
</style>

View File

@ -1,52 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
size?: 'small' | 'medium' | 'large'
color?: string
}
const props = withDefaults(defineProps<Props>(), {
size: 'medium',
color: 'var(--primary-color)',
})
const spinnerSize = computed(() => {
const sizes = {
small: '24px',
medium: '40px',
large: '60px',
}
return sizes[props.size]
})
const borderWidth = computed(() => {
const widths = {
small: '3px',
medium: '4px',
large: '5px',
}
return widths[props.size]
})
</script>
<template>
<div
class="spinner"
:style="{
width: spinnerSize,
height: spinnerSize,
borderWidth: borderWidth,
borderTopColor: color,
}"
></div>
</template>
<style lang="scss" scoped>
.spinner {
border: 4px solid var(--border-light);
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
</style>

View File

@ -1,7 +1,25 @@
import LayoutVue from './common/LayoutVue.vue'
import ButtonCommon from './common/ButtonCommon.vue'
import FormCommon from './common/FormCommon.vue'
import SpinnerCommon from './common/SpinnerCommon.vue'
import OnboardingComponent from './auth/OnboardingComponent.vue'
import OpenWalletComponent from './auth/OpenWalletComponent.vue'
import CreateWalletComponent from './auth/CreateWalletComponent.vue'
import RecoverySeedComponent from './auth/RecoverySeedComponent.vue'
import ConfirmSeedComponent from './auth/ConfirmSeedComponent.vue'
import ImportWalletComponent from './auth/ImportWalletComponent.vue'
import KeystoreDownloadComponent from './auth/KeystoreDownloadComponent.vue'
import { IconCommon } from './icon'
export { LayoutVue, ButtonCommon, FormCommon, SpinnerCommon, IconCommon }
export {
LayoutVue,
ButtonCommon,
FormCommon,
OnboardingComponent,
OpenWalletComponent,
CreateWalletComponent,
RecoverySeedComponent,
ConfirmSeedComponent,
IconCommon,
ImportWalletComponent,
KeystoreDownloadComponent,
}

View File

@ -1,292 +0,0 @@
import { computed } from 'vue'
import { useNeptuneStore } from '@/stores/neptuneStore'
import * as API from '@/api/neptuneApi'
import type { GenerateSeedResult, ViewKeyResult } from '@/interface'
import initWasm, {
generate_seed,
get_viewkey,
address_from_seed,
validate_seed_phrase,
decode_viewkey,
} from '@neptune/wasm'
let wasmInitialized = false
let initPromise: Promise<void> | null = null
export function useNeptuneWallet() {
const store = useNeptuneStore()
// ===== WASM METHODS =====
const ensureWasmInitialized = async (): Promise<void> => {
if (wasmInitialized) {
return
}
if (initPromise) {
return initPromise
}
initPromise = (async () => {
try {
store.setLoading(true)
store.setError(null)
await initWasm()
wasmInitialized = true
} catch (err) {
wasmInitialized = false
const errorMsg = 'Failed to initialize Neptune WASM'
store.setError(errorMsg)
console.error('WASM init error:', err)
throw new Error(errorMsg)
} finally {
store.setLoading(false)
}
})()
return initPromise
}
const generateWallet = async (): Promise<GenerateSeedResult> => {
try {
store.setLoading(true)
store.setError(null)
await ensureWasmInitialized()
const resultJson = generate_seed()
const result: GenerateSeedResult = JSON.parse(resultJson)
store.setSeedPhrase(result.seed_phrase)
store.setReceiverId(result.receiver_identifier)
const viewKeyResult = await getViewKeyFromSeed(result.seed_phrase, store.getNetwork)
store.setViewKey(viewKeyResult.view_key)
store.setAddress(viewKeyResult.address)
return result
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to generate wallet'
store.setError(errorMsg)
throw err
} finally {
store.setLoading(false)
}
}
const getViewKeyFromSeed = async (
seedPhrase: string[],
network: 'mainnet' | 'testnet'
): Promise<ViewKeyResult> => {
await ensureWasmInitialized()
const seedPhraseJson = JSON.stringify(seedPhrase)
const resultJson = get_viewkey(seedPhraseJson, network)
return JSON.parse(resultJson)
}
const importWallet = async (
seedPhrase: string[],
network: 'mainnet' | 'testnet' = 'testnet'
): Promise<ViewKeyResult> => {
try {
store.setLoading(true)
store.setError(null)
const isValid = await validateSeedPhrase(seedPhrase)
if (!isValid) {
throw new Error('Invalid seed phrase')
}
const result = await getViewKeyFromSeed(seedPhrase, network)
store.setSeedPhrase(seedPhrase)
store.setReceiverId(result.receiver_identifier)
store.setViewKey(result.view_key)
store.setAddress(result.address)
store.setNetwork(network)
return result
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to import wallet'
store.setError(errorMsg)
throw err
} finally {
store.setLoading(false)
}
}
const getAddressFromSeed = async (
seedPhrase: string[],
network: 'mainnet' | 'testnet'
): Promise<string> => {
await ensureWasmInitialized()
const seedPhraseJson = JSON.stringify(seedPhrase)
return address_from_seed(seedPhraseJson, network)
}
const validateSeedPhrase = async (seedPhrase: string[]): Promise<boolean> => {
try {
await ensureWasmInitialized()
const seedPhraseJson = JSON.stringify(seedPhrase)
return validate_seed_phrase(seedPhraseJson)
} catch (err) {
console.error('Validation error:', err)
return false
}
}
const decodeViewKey = async (viewKeyHex: string): Promise<{ receiver_identifier: string }> => {
await ensureWasmInitialized()
const resultJson = decode_viewkey(viewKeyHex)
return JSON.parse(resultJson)
}
// ===== API METHODS =====
const getUtxos = async (
startBlock: number = 0,
endBlock: number | null = null,
maxSearchDepth: number = 1000
): Promise<any> => {
try {
if (!store.getViewKey) {
throw new Error('No view key available. Please import or generate a wallet first.')
}
store.setLoading(true)
store.setError(null)
const response = await API.getUtxosFromViewKey(
store.getViewKey,
startBlock,
endBlock,
maxSearchDepth
)
const result = response.data?.result || response.data
store.setUtxos(result.utxos || result || [])
return result
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to get UTXOs'
store.setError(errorMsg)
throw err
} finally {
store.setLoading(false)
}
}
const getBalance = async (): Promise<any> => {
try {
store.setLoading(true)
store.setError(null)
const response = await API.getBalance()
const result = response.data?.result || response.data
store.setBalance(result.balance || result)
return result
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to get balance'
store.setError(errorMsg)
throw err
} finally {
store.setLoading(false)
}
}
const getBlockHeight = async (): Promise<any> => {
try {
store.setLoading(true)
store.setError(null)
const response = await API.getBlockHeight()
const result = response.data?.result || response.data
return result.height || result
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to get block height'
store.setError(errorMsg)
throw err
} finally {
store.setLoading(false)
}
}
const getNetworkInfo = async (): Promise<any> => {
try {
store.setLoading(true)
store.setError(null)
const response = await API.getNetworkInfo()
const result = response.data?.result || response.data
return result
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to get network info'
store.setError(errorMsg)
throw err
} finally {
store.setLoading(false)
}
}
const sendTransaction = async (
toAddress: string,
amount: string,
fee: string
): Promise<any> => {
try {
store.setLoading(true)
store.setError(null)
const response = await API.sendTransaction(toAddress, amount, fee)
const result = response.data?.result || response.data
return result
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to send transaction'
store.setError(errorMsg)
throw err
} finally {
store.setLoading(false)
}
}
const setNetwork = async (network: 'mainnet' | 'testnet') => {
store.setNetwork(network)
if (store.getSeedPhrase) {
const viewKeyResult = await getViewKeyFromSeed(store.getSeedPhrase, network)
store.setAddress(viewKeyResult.address)
store.setViewKey(viewKeyResult.view_key)
}
}
// ===== UTILITY METHODS =====
const clearWallet = () => {
store.clearWallet()
}
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,
clearWallet,
setNetwork,
}
}

View File

@ -1,3 +1,2 @@
export * from './common'
export * from './home'
export * from './neptune'

View File

@ -1,20 +0,0 @@
export interface WalletState {
seedPhrase: string[] | null
receiverId: string | null
viewKey: string | null
address: string | null
network: 'mainnet' | 'testnet'
balance: string | null
utxos: any[]
}
export interface GenerateSeedResult {
seed_phrase: string[]
receiver_identifier: string
}
export interface ViewKeyResult {
receiver_identifier: string
view_key: string
address: string
}

View File

@ -1,26 +1,45 @@
import * as Page from '@/views'
import { useNeptuneStore } from '@/stores/neptuneStore'
import { getToken } from '@/utils'
export const ifAuthenticated = (to: any, from: any, next: any) => {
const neptuneStore = useNeptuneStore()
if (neptuneStore.getReceiverId) {
const ifAuthenticated = (to: any, from: any, next: any) => {
if (getToken()) {
next()
return
}
next('/login')
}
const ifNotAuthenticated = (to: any, from: any, next: any) => {
if (!getToken()) {
next()
return
}
next('/')
}
export const routes: any = [
{
path: '/',
name: 'home',
name: 'index',
component: Page.Home,
beforeEnter: ifAuthenticated,
},
{
path: '/login',
name: 'login',
component: Page.Auth,
beforeEnter: ifNotAuthenticated,
},
{
path: '/recovery-seed',
name: 'recovery-seed',
component: Page.Auth,
beforeEnter: ifNotAuthenticated,
},
{
path: '/confirm-seed',
name: 'confirm-seed',
component: Page.Auth,
beforeEnter: ifNotAuthenticated,
},
{
path: '/:pathMatch(.*)*',

View File

@ -1,3 +1,2 @@
export * from './seedStore'
export * from './authStore'
export * from './neptuneStore'

View File

@ -1,113 +0,0 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { WalletState } from '@/interface'
export const useNeptuneStore = defineStore('neptune', () => {
// ===== STATE =====
const wallet = ref<WalletState>({
seedPhrase: null,
receiverId: null,
viewKey: null,
address: null,
network: 'testnet',
balance: null,
utxos: [],
})
const isLoading = ref(false)
const error = ref<string | null>(null)
// ===== SETTERS =====
const setSeedPhrase = (seedPhrase: string[] | null) => {
wallet.value.seedPhrase = seedPhrase
}
const setReceiverId = (receiverId: string | null) => {
wallet.value.receiverId = receiverId
}
const setViewKey = (viewKey: string | null) => {
wallet.value.viewKey = viewKey
}
const setAddress = (address: string | null) => {
wallet.value.address = address
}
const setNetwork = (network: 'mainnet' | 'testnet') => {
wallet.value.network = network
}
const setBalance = (balance: string | null) => {
wallet.value.balance = balance
}
const setUtxos = (utxos: any[]) => {
wallet.value.utxos = utxos
}
const setLoading = (loading: boolean) => {
isLoading.value = loading
}
const setError = (err: string | null) => {
error.value = err
}
const setWallet = (walletData: Partial<WalletState>) => {
wallet.value = { ...wallet.value, ...walletData }
}
const clearWallet = () => {
wallet.value = {
seedPhrase: null,
receiverId: null,
viewKey: null,
address: null,
network: 'testnet',
balance: null,
utxos: [],
}
error.value = null
}
// ===== GETTERS =====
const getWallet = computed(() => wallet.value)
const getSeedPhrase = computed(() => wallet.value.seedPhrase)
const getReceiverId = computed(() => wallet.value.receiverId)
const getViewKey = computed(() => wallet.value.viewKey)
const getAddress = computed(() => wallet.value.address)
const getNetwork = computed(() => wallet.value.network)
const getBalance = computed(() => wallet.value.balance)
const getUtxos = computed(() => wallet.value.utxos)
const hasWallet = computed(() => wallet.value.address !== null)
const getLoading = computed(() => isLoading.value)
const getError = computed(() => error.value)
return {
getWallet,
getSeedPhrase,
getReceiverId,
getViewKey,
getAddress,
getNetwork,
getBalance,
getUtxos,
hasWallet,
getLoading,
getError,
setSeedPhrase,
setReceiverId,
setViewKey,
setAddress,
setNetwork,
setBalance,
setUtxos,
setLoading,
setError,
setWallet,
clearWallet,
}
})

View File

@ -1,39 +0,0 @@
/**
* Type declarations for Neptune WASM module
*/
declare module '/wasm/neptune_wasm.js' {
export default function init(): Promise<void>
export function generate_seed(): string
export function get_viewkey(seedPhraseJson: string, network: string): string
export function address_from_seed(seedPhraseJson: string, network: string): string
export function validate_seed_phrase(seedPhraseJson: string): boolean
export function decode_viewkey(viewKeyHex: string): string
export function get_balance(rpcUrl: string): Promise<string>
export function get_block_height(rpcUrl: string): Promise<number>
export function get_network_info(rpcUrl: string): Promise<string>
export function get_wallet_address(rpcUrl: string): Promise<string>
export function get_utxos_from_viewkey(
rpcUrl: string,
viewKeyHex: string,
startBlock: number,
endBlock: number | null
): Promise<string>
export function build_and_sign_tx(requestJson: string): string
export function send_tx_jsonrpc(
rpcUrl: string,
toAddress: string,
amount: string,
fee: string
): Promise<string>
}
// Global type definitions
declare global {
interface Window {
neptuneWasm?: any
}
}
export {}

View File

@ -131,12 +131,3 @@ export const validateSeedPhrase18 = (words: string[]): boolean => {
if (words.length !== 18) return false
return words.every((word) => BIP39_WORDS.includes(word.toLowerCase()))
}
export const generateSeedPhrase18 = (): string[] => {
const words: string[] = []
for (let i = 0; i < 18; i++) {
const randomIndex = Math.floor(Math.random() * BIP39_WORDS.length)
words.push(BIP39_WORDS[randomIndex])
}
return words
}

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { OnboardingComponent } from './components'
import { OnboardingComponent } from '@/components'
import { useAuthStore } from '@/stores'
import { LoginTab, CreateTab, RecoveryTab, ConfirmTab } from './components'

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ConfirmSeedComponent } from '.'
import { ConfirmSeedComponent } from '@/components'
const emit = defineEmits<{
next: []

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { CreateWalletComponent } from '.'
import { CreateWalletComponent } from '@/components'
const emit = defineEmits<{
goToLogin: []

View File

@ -1,176 +0,0 @@
<script setup lang="ts">
import { ref, defineEmits, onMounted } from 'vue'
import { KeystoreDownloadComponent, RecoverySeedComponent, ConfirmSeedComponent } from '.'
import { ChooseBackupMethodStep, CreatePasswordStep, WalletCreatedStep } from './steps'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { useNeptuneStore } from '@/stores/neptuneStore'
import { message } from 'ant-design-vue'
import { useRouter } from 'vue-router'
const emit = defineEmits<{
navigateToOpenWallet: [event: Event]
navigateToRecoverySeed: []
}>()
const { initWasm, generateWallet } = useNeptuneWallet()
const neptuneStore = useNeptuneStore()
const router = useRouter()
const step = ref(1)
const backupMethod = ref<'none' | 'seed' | 'keystore'>('none')
onMounted(async () => {
try {
await initWasm()
} catch (err) {
message.error('Failed to initialize wallet. Please refresh the page.')
}
})
const handleIHaveWallet = (e: Event) => {
e.preventDefault()
emit('navigateToOpenWallet', e)
}
const handleChooseMethod = async (method: 'seed' | 'keystore'): Promise<void> => {
backupMethod.value = method
if (method === 'seed') {
try {
await generateWallet()
message.success('Wallet generated successfully!')
} catch (err) {
console.error('❌ Failed to generate wallet:', err)
message.error('Failed to generate wallet. Please try again.')
return
}
}
step.value = 2
}
const handleNextFromRecoverySeed = () => {
step.value = 3
}
const handleNextFromConfirmSeed = () => {
step.value = 4
}
const handleNextFromPassword = () => {
step.value = 3
}
function downloadKeystoreFile() {
const data = {
account: 'neptune-wallet',
version: 1,
enc: 'mock-data',
created: new Date().toISOString(),
note: 'Exported from web-wallet',
hint: 'Replace bằng file thực tế trong tích hợp thật.',
}
const file = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const link = document.createElement('a')
link.href = URL.createObjectURL(file)
link.download = 'neptune-wallet-keystore.json'
link.click()
step.value = 4
}
const handleBackToChoose = () => {
backupMethod.value = 'none'
neptuneStore.clearWallet()
step.value = 1
}
const handleBackToRecoverySeed = () => {
step.value = 2
}
const handleBackToPassword = () => {
step.value = 2
}
const handleAccessWallet = () => {
router.push({ name: 'home' })
}
function resetAll() {
step.value = 1
backupMethod.value = 'none'
neptuneStore.clearWallet()
}
</script>
<template>
<div class="auth-container">
<div class="auth-card">
<!-- Step 1: Choose Backup Method -->
<ChooseBackupMethodStep v-if="step === 1" @choose-method="handleChooseMethod" />
<!-- Step 2: -->
<template v-else-if="step === 2">
<!-- Seed flow: Show recovery seed -->
<RecoverySeedComponent
v-if="backupMethod === 'seed'"
@next="handleNextFromRecoverySeed"
@back="handleBackToChoose"
/>
<!-- Keystore flow: Create password -->
<CreatePasswordStep
v-else-if="backupMethod === 'keystore'"
@next="handleNextFromPassword"
@navigate-to-open-wallet="handleIHaveWallet"
/>
</template>
<!-- Step 3: Based on chosen method -->
<template v-else-if="step === 3">
<ConfirmSeedComponent
v-if="backupMethod === 'seed'"
@next="handleNextFromConfirmSeed"
@back="handleBackToRecoverySeed"
/>
<KeystoreDownloadComponent
v-else-if="backupMethod === 'keystore'"
@download="downloadKeystoreFile"
@back="handleBackToPassword"
/>
</template>
<!-- Step 4: Success -->
<WalletCreatedStep
v-else-if="step === 4"
@access-wallet="handleAccessWallet"
@create-another="resetAll"
/>
<!-- Fallback slot -->
<template v-else>
<slot></slot>
</template>
</div>
</div>
</template>
<style lang="scss" scoped>
.auth-container {
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-xl);
background: var(--bg-light);
}
.auth-card {
@include card-base;
max-width: 720px;
width: 100%;
@include screen(mobile) {
max-width: 100%;
padding: var(--spacing-md);
}
}
</style>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ImportWalletComponent, OpenWalletComponent } from '.'
import { ImportWalletComponent, OpenWalletComponent } from '@/components'
const emit = defineEmits<{ goToCreate: [] }>()

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { RecoverySeedComponent } from '.'
import { RecoverySeedComponent } from '@/components'
const emit = defineEmits<{
next: []

View File

@ -1,17 +1,4 @@
// Tabs
export { default as LoginTab } from './LoginTab.vue'
export { default as CreateTab } from './CreateTab.vue'
export { default as RecoveryTab } from './RecoveryTab.vue'
export { default as ConfirmTab } from './ConfirmTab.vue'
// Auth Components
export { default as OnboardingComponent } from './OnboardingComponent.vue'
export { default as OpenWalletComponent } from './OpenWalletComponent.vue'
export { default as CreateWalletComponent } from './CreateWalletComponent.vue'
export { default as RecoverySeedComponent } from './RecoverySeedComponent.vue'
export { default as ConfirmSeedComponent } from './ConfirmSeedComponent.vue'
export { default as ImportWalletComponent } from './ImportWalletComponent.vue'
export { default as KeystoreDownloadComponent } from './KeystoreDownloadComponent.vue'
// Steps
export * from './steps'

View File

@ -1,46 +0,0 @@
<script setup lang="ts">
import { ButtonCommon } from '@/components'
const emit = defineEmits<{
chooseMethod: [method: 'seed' | 'keystore']
}>()
const handleChooseMethod = (method: 'seed' | 'keystore') => {
emit('chooseMethod', method)
}
</script>
<template>
<div class="choose-backup-method">
<h2>Choose your method</h2>
<div class="choose-btns">
<ButtonCommon type="primary" size="large" @click="() => handleChooseMethod('seed')">
Mnemonic Phrase
</ButtonCommon>
<ButtonCommon type="default" size="large" @click="() => handleChooseMethod('keystore')">
Keystore File
</ButtonCommon>
</div>
</div>
</template>
<style lang="scss" scoped>
.choose-backup-method {
text-align: center;
padding: 20px 8px;
h2 {
color: var(--primary-color);
font-weight: 700;
letter-spacing: 0.07em;
margin-bottom: 1px;
}
.choose-btns {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 15px;
}
}
</style>

View File

@ -1,384 +0,0 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ButtonCommon, FormCommon } from '@/components'
const emit = defineEmits<{
next: []
navigateToOpenWallet: [event: Event]
}>()
const password = ref('')
const confirmPassword = ref('')
const passwordError = ref('')
const confirmPasswordError = ref('')
const passwordStrength = computed(() => {
if (!password.value) return { level: 0, text: '', color: '' }
let strength = 0
const checks = {
length: password.value.length >= 8,
uppercase: /[A-Z]/.test(password.value),
lowercase: /[a-z]/.test(password.value),
number: /[0-9]/.test(password.value),
special: /[!@#$%^&*(),.?":{}|<>]/.test(password.value),
}
strength = Object.values(checks).filter(Boolean).length
if (strength <= 2) return { level: 1, text: 'Weak', color: 'var(--error-color)' }
if (strength <= 3) return { level: 2, text: 'Medium', color: 'var(--warning-color)' }
if (strength <= 4) return { level: 3, text: 'Good', color: 'var(--info-color)' }
return { level: 4, text: 'Strong', color: 'var(--success-color)' }
})
const isPasswordMatch = computed(() => {
if (!confirmPassword.value) return true
return password.value === confirmPassword.value
})
const canProceed = computed(() => {
return (
password.value.length >= 8 &&
confirmPassword.value.length >= 8 &&
isPasswordMatch.value &&
passwordStrength.value.level >= 2
)
})
const handleNext = () => {
if (!canProceed.value) {
if (password.value.length < 8) {
passwordError.value = 'Password must be at least 8 characters'
}
if (!isPasswordMatch.value) {
confirmPasswordError.value = 'Passwords do not match'
}
return
}
emit('next')
}
const handleIHaveWallet = (e: Event) => {
e.preventDefault()
emit('navigateToOpenWallet', e)
}
</script>
<template>
<div class="create-password-step">
<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="neptuneGradient"
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="ringGradient" 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(#neptuneGradient)" />
<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(#ringGradient)"
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">NPTUN</span>
</div>
</div>
<h1 class="auth-title">Create New Wallet</h1>
<p class="auth-subtitle">Secure your wallet with a strong password</p>
</div>
<div class="auth-card-content">
<div class="form-group">
<FormCommon
v-model="password"
type="password"
label="Create Password"
placeholder="Enter your password"
show-password-toggle
required
:error="passwordError"
@input="passwordError = ''"
/>
<div v-if="password" class="password-strength">
<div class="strength-bar">
<div
class="strength-fill"
:style="{
width: `${(passwordStrength.level / 4) * 100}%`,
backgroundColor: passwordStrength.color,
}"
></div>
</div>
<span class="strength-text" :style="{ color: passwordStrength.color }">{{
passwordStrength.text
}}</span>
</div>
</div>
<div class="form-group">
<FormCommon
v-model="confirmPassword"
type="password"
label="Confirm Password"
placeholder="Re-enter your password"
show-password-toggle
required
:error="confirmPasswordError"
@input="confirmPasswordError = ''"
/>
<div
v-if="confirmPassword"
class="password-match"
:class="{ match: isPasswordMatch }"
>
<span v-if="isPasswordMatch" class="match-text"> Passwords match </span>
<span v-else class="match-text error"> Passwords do not match </span>
</div>
</div>
<p class="helper-text">
Password must be at least 8 characters with uppercase, lowercase, and numbers.
</p>
<div class="auth-button-group">
<ButtonCommon
type="primary"
size="large"
class="auth-button"
block
:disabled="!canProceed"
@click="handleNext"
>
Create Wallet
</ButtonCommon>
<div class="secondary-actions">
<button class="link-button" @click="handleIHaveWallet">
Already have a wallet?
</button>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.create-password-step {
width: 100%;
}
.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;
}
}
.auth-card-content {
.form-group {
margin-bottom: var(--spacing-xl);
}
}
.password-strength {
margin-top: var(--spacing-sm);
display: flex;
align-items: center;
gap: var(--spacing-md);
.strength-bar {
flex: 1;
height: 4px;
background: var(--border-light);
border-radius: var(--radius-full);
overflow: hidden;
.strength-fill {
height: 100%;
transition: all 0.3s ease;
}
}
.strength-text {
font-size: var(--font-xs);
font-weight: var(--font-medium);
min-width: 50px;
text-align: right;
}
}
.password-match {
margin-top: var(--spacing-sm);
font-size: var(--font-xs);
&.match .match-text {
color: var(--success-color);
}
.match-text.error {
color: var(--error-color);
}
}
.helper-text {
font-size: var(--font-xs);
color: var(--text-muted);
margin: 0 0 var(--spacing-xl);
line-height: var(--leading-normal);
}
.auth-button {
width: fit-content;
margin: 0 auto;
}
.auth-button-group {
margin-top: var(--spacing-2xl);
display: flex;
flex-direction: column;
.secondary-actions {
display: flex;
justify-content: center;
align-items: center;
gap: var(--spacing-sm);
margin-top: var(--spacing-md);
}
.link-button {
background: none;
border: none;
color: var(--primary-color);
font-size: var(--font-sm);
cursor: pointer;
transition: color 0.2s ease;
padding: 0;
&:hover {
color: var(--primary-hover);
text-decoration: underline;
}
}
.separator {
color: var(--text-muted);
font-size: var(--font-sm);
}
}
@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>

View File

@ -1,125 +0,0 @@
<script setup lang="ts">
import { ButtonCommon } from '@/components'
const emit = defineEmits<{
accessWallet: []
createAnother: []
}>()
const handleAccessWallet = () => {
emit('accessWallet')
}
const handleCreateAnother = () => {
emit('createAnother')
}
</script>
<template>
<div class="well-done-step">
<h2 class="done-main">You are done!</h2>
<p class="done-desc">
You are now ready to take advantage of all that your wallet has to offer! Access with
keystore file should only be used in an offline setting.
</p>
<div class="center-svg" style="margin: 14px auto 12px auto">
<svg width="180" height="95" viewBox="0 0 175 92" fill="none">
<rect x="111" y="37" width="64" height="33" rx="7" fill="#23B1EC" />
<rect
x="30.5"
y="37.5"
width="80"
height="46"
rx="7.5"
fill="#D6F9FE"
stroke="#AEEBF8"
stroke-width="5"
/>
<rect x="56" y="67" width="32" height="10" rx="3" fill="#B0F3A6" />
<rect x="46" y="49" width="52" height="12" rx="3" fill="#a2d2f5" />
<circle cx="155" cy="52" r="8" fill="#fff" />
<rect x="121" y="43" width="27" height="7" rx="1.5" fill="#5AE9D2" />
<rect x="128" y="59" width="17" height="4" rx="1.5" fill="#FCEBBA" />
<circle cx="40" cy="27" r="7" fill="#A2D2F5" />
<g>
<circle cx="128" cy="21" r="3" fill="#FF8585" />
<circle cx="57.5" cy="20.5" r="1.5" fill="#67DEFF" />
<rect x="95" y="18" width="7" height="5" rx="2" fill="#A2D2F5" />
</g>
</svg>
</div>
<div class="btn-row">
<ButtonCommon
class="done-btn"
type="primary"
size="large"
block
style="margin-bottom: 0.3em"
@click="handleAccessWallet"
>
Access Wallet
</ButtonCommon>
<button class="done-link" type="button" @click="handleCreateAnother">
Create Another Wallet
</button>
</div>
</div>
</template>
<style lang="scss" scoped>
.well-done-step {
text-align: center;
padding: 20px 8px;
.done-title {
color: var(--primary-color);
font-weight: 700;
letter-spacing: 0.07em;
margin-bottom: 1px;
}
.done-main {
font-size: 1.36rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 2px;
}
.done-desc {
color: var(--text-secondary);
font-size: 1.11em;
max-width: 410px;
margin: 2px auto 15px auto;
}
.center-svg {
display: flex;
justify-content: center;
}
.btn-row {
display: flex;
flex-direction: column;
gap: 11px;
align-items: center;
width: 100%;
max-width: 400px;
margin: 0 auto 5px auto;
}
.done-btn {
margin-bottom: 0.3em;
}
.done-link {
background: none;
border: none;
color: var(--primary-color);
font-size: 1em;
text-decoration: underline;
cursor: pointer;
margin: 0 auto;
font-weight: 600;
}
}
</style>

View File

@ -1,3 +0,0 @@
export { default as ChooseBackupMethodStep } from './ChooseBackupMethodStep.vue'
export { default as CreatePasswordStep } from './CreatePasswordStep.vue'
export { default as WalletCreatedStep } from './WalletCreatedStep.vue'

View File

@ -46,11 +46,7 @@ const network = ref('kaspa-mainnet')
padding: var(--spacing-lg);
font-family: var(--font-primary);
@include screen(tablet) {
padding: var(--spacing-2xl);
}
@include screen(desktop) {
@media (min-width: 768px) {
padding: var(--spacing-2xl);
}
}
@ -66,7 +62,7 @@ const network = ref('kaspa-mainnet')
letter-spacing: var(--tracking-wide);
padding: 10px 16px;
@include screen(mobile) {
@media (max-width: 768px) {
font-size: 12px;
padding: 8px 12px;
}

View File

@ -1,83 +1,111 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
import { formatNumberToLocaleString } from '@/utils'
import { SpinnerCommon } from '@/components'
import { useNeptuneStore } from '@/stores/neptuneStore'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { message } from 'ant-design-vue'
import type { NetworkStatus } from '@/interface'
const neptuneStore = useNeptuneStore()
const { getBlockHeight, getNetworkInfo } = useNeptuneWallet()
const blockHeight = ref(0)
const networkInfo = ref<any>(null)
const loading = ref(true)
const error = ref('')
const lastUpdate = ref<Date | null>(null)
// let pollingInterval: number | null = null
const network = computed(() => neptuneStore.getNetwork)
const loadNetworkData = async () => {
try {
error.value = ''
const [heightResult, infoResult] = await Promise.all([getBlockHeight(), getNetworkInfo()])
if (heightResult !== undefined) {
blockHeight.value =
typeof heightResult === 'number'
? heightResult
: heightResult.height || heightResult || 0
}
if (infoResult) {
networkInfo.value = infoResult
}
lastUpdate.value = new Date()
if (loading.value) {
loading.value = false
}
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to load network data'
error.value = errorMsg
loading.value = false
message.error(errorMsg)
}
}
const retryConnection = async () => {
loading.value = true
error.value = ''
await loadNetworkData()
}
// const startPolling = () => {
// pollingInterval = window.setInterval(() => {
// if (!loading.value) {
// loadNetworkData()
// }
// }, 10000)
// }
// const stopPolling = () => {
// if (pollingInterval) {
// clearInterval(pollingInterval)
// pollingInterval = null
// }
// }
onMounted(async () => {
await loadNetworkData()
// startPolling()
const networkStatus = ref<NetworkStatus>({
network: 'kaspa-mainnet',
daaScore: 0,
dagHeader: 0,
dagBlocks: 0,
difficulty: 0,
medianOffset: '00:00:00',
medianTimeUTC: '',
})
// onUnmounted(() => {
// stopPolling()
// })
const loading = ref(true)
const error = ref('')
const isConnected = ref(false)
let rpcClient: any = null
let unsubscribe: (() => void) | null = null
// Initialize Kaspa RPC connection
const initializeKaspaRPC = async () => {
try {
loading.value = true
error.value = ''
await simulateRPCConnection()
isConnected.value = true
loading.value = false
} catch (err) {
error.value = 'Failed to connect to Kaspa network'
loading.value = false
isConnected.value = false
useMockData()
}
}
const simulateRPCConnection = async (): Promise<void> => {
return new Promise((resolve) => {
setTimeout(() => {
// Initial data
networkStatus.value = {
network: 'kaspa-mainnet',
daaScore: 256315320,
dagHeader: 1437265,
dagBlocks: 1437265,
difficulty: 33048964118340300.0,
medianOffset: '00:00:00',
medianTimeUTC: new Date().toISOString().replace('T', ' ').substring(0, 19),
}
const mockSubscription = setInterval(() => {
networkStatus.value.daaScore += 1
networkStatus.value.dagHeader += 1
networkStatus.value.dagBlocks += 1
updateMedianTime()
}, 1000)
unsubscribe = () => clearInterval(mockSubscription)
resolve()
}, 1000)
})
}
const updateMedianTime = () => {
networkStatus.value.medianTimeUTC = new Date().toISOString().replace('T', ' ').substring(0, 19)
}
const useMockData = () => {
networkStatus.value = {
network: 'kaspa-mainnet',
daaScore: 256315320,
dagHeader: 1437265,
dagBlocks: 1437265,
difficulty: 33048964118340300.0,
medianOffset: '00:00:00',
medianTimeUTC: new Date().toISOString().replace('T', ' ').substring(0, 19),
}
}
const retryConnection = () => {
initializeKaspaRPC()
}
const cleanup = () => {
if (unsubscribe) {
unsubscribe()
unsubscribe = null
}
if (rpcClient) {
rpcClient.disconnect()
rpcClient = null
}
isConnected.value = false
}
onMounted(() => {
initializeKaspaRPC()
})
onUnmounted(() => {
cleanup()
})
</script>
<template>
@ -86,8 +114,8 @@ onMounted(async () => {
<h2 class="section-title">NETWORK STATUS</h2>
<!-- Loading State -->
<div v-if="loading && !blockHeight" class="loading-state">
<SpinnerCommon size="medium" />
<div v-if="loading && networkStatus.daaScore === 0" class="loading-state">
<div class="spinner"></div>
<p>Loading network data...</p>
</div>
@ -101,32 +129,53 @@ onMounted(async () => {
<div v-else class="status-grid">
<div class="status-item">
<span class="status-label">Network</span>
<span class="status-value">Neptune {{ network }}</span>
<span class="status-value">{{ networkStatus.network }}</span>
</div>
<div class="status-item">
<span class="status-label">Block Height</span>
<span class="status-value">{{ formatNumberToLocaleString(blockHeight) }}</span>
<span class="status-label">DAA Score</span>
<span class="status-value">{{
formatNumberToLocaleString(networkStatus.daaScore)
}}</span>
</div>
<div v-if="networkInfo?.version" class="status-item">
<span class="status-label">Version</span>
<span class="status-value">{{ networkInfo.version }}</span>
<div class="status-item">
<span class="status-label">DAG Header</span>
<span class="status-value">{{
formatNumberToLocaleString(networkStatus.dagHeader)
}}</span>
</div>
<div v-if="networkInfo?.peer_count !== undefined" class="status-item">
<span class="status-label">Peers</span>
<span class="status-value">{{ networkInfo.peer_count }}</span>
<div class="status-item">
<span class="status-label">DAG Blocks</span>
<span class="status-value">{{
formatNumberToLocaleString(networkStatus.dagBlocks)
}}</span>
</div>
<div v-if="networkInfo?.chain_id" class="status-item">
<span class="status-label">Chain ID</span>
<span class="status-value">{{ networkInfo.chain_id }}</span>
<div class="status-item">
<span class="status-label">Difficulty</span>
<span class="status-value">{{
formatNumberToLocaleString(networkStatus.difficulty)
}}</span>
</div>
<div v-if="lastUpdate" class="status-item">
<span class="status-label">Last Updated</span>
<span class="status-value">{{ lastUpdate.toLocaleTimeString() }}</span>
<div class="status-item">
<span class="status-label">Median Offset</span>
<span class="status-value">{{ networkStatus.medianOffset }}</span>
</div>
<div class="status-item">
<span class="status-label">Median Time UTC</span>
<span class="status-value">{{ networkStatus.medianTimeUTC }}</span>
</div>
<!-- Last Update Indicator -->
<div class="update-indicator">
<span class="update-dot" :class="{ connected: isConnected }"></span>
<span class="update-text">
{{ isConnected ? 'Connected - Live updates' : 'Connecting...' }}
</span>
</div>
</div>
</div>
@ -187,14 +236,29 @@ onMounted(async () => {
// Loading State
.loading-state {
@include center_flex;
text-align: center;
padding: var(--spacing-4xl);
color: var(--text-secondary);
.spinner {
width: 40px;
height: 40px;
margin: 0 auto var(--spacing-lg);
border: 4px solid var(--border-light);
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
p {
margin: 0;
font-size: var(--font-md);
}
}
// Error State
.error-state {
@include center_flex;
text-align: center;
padding: var(--spacing-4xl);
color: var(--error-color);
@ -218,5 +282,34 @@ onMounted(async () => {
}
}
}
// Update Indicator
.update-indicator {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
margin-top: var(--spacing-2xl);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--border-color);
.update-dot {
width: 8px;
height: 8px;
background: var(--text-muted);
border-radius: 50%;
transition: var(--transition-all);
&.connected {
background: var(--success-color);
animation: pulse-dot 2s infinite;
}
}
.update-text {
font-size: var(--font-xs);
color: var(--text-muted);
}
}
}
</style>

View File

@ -1,12 +1,10 @@
<script setup lang="ts">
import { ref } from 'vue'
import { EditOutlined } from '@ant-design/icons-vue'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
const { getUtxos } = useNeptuneWallet()
const inUseUtxosCount = ref(0)
const inUseUtxosAmount = ref(0)
</script>
<template>

View File

@ -1,20 +1,36 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Divider } from 'ant-design-vue'
import ButtonCommon from '@/components/common/ButtonCommon.vue'
import type { WalletTabProps } from '@/interface'
const props = defineProps<WalletTabProps>()
const walletVersion = ref('1.1.38')
const walletStatus = ref('Online')
const networkName = computed(() => props.network.replace('-mainnet', ''))
const handleBackupFile = () => {
// TODO: Implement backup file functionality
console.log('Backup File')
}
const handleBackupSeed = () => {
// TODO: Implement backup seed functionality
console.log('Backup Seed')
}
</script>
<template>
<div class="content-card wallet-info-card">
<div class="wallet-header">
<h2 class="wallet-title">NEPTUNE WALLET</h2>
<h2 class="wallet-title">KASPA WALLET</h2>
<p class="wallet-version">Version {{ walletVersion }}</p>
<p class="wallet-status-text">
Status: <strong>{{ walletStatus }}</strong>
</p>
<p class="wallet-network">
Network: <strong>{{ networkName }}</strong>
</p>
</div>
<div class="wallet-actions">

View File

@ -9,9 +9,6 @@ export default defineConfig({
port: 3008,
},
plugins: [vue(), vueJsx(), VueDevTools()],
optimizeDeps: {
exclude: ['@neptune/wasm'],
},
css: {
preprocessorOptions: {
scss: {

209
yarn.lock
View File

@ -37,24 +37,24 @@
picocolors "^1.1.1"
"@babel/compat-data@^7.27.2":
version "7.28.5"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.5.tgz#a8a4962e1567121ac0b3b487f52107443b455c7f"
integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.4.tgz#96fdf1af1b8859c8474ab39c295312bfb7c24b04"
integrity sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==
"@babel/core@^7.23.0", "@babel/core@^7.23.3":
version "7.28.5"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.5.tgz#4c81b35e51e1b734f510c99b07dfbc7bbbb48f7e"
integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.4.tgz#12a550b8794452df4c8b084f95003bce1742d496"
integrity sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==
dependencies:
"@babel/code-frame" "^7.27.1"
"@babel/generator" "^7.28.5"
"@babel/generator" "^7.28.3"
"@babel/helper-compilation-targets" "^7.27.2"
"@babel/helper-module-transforms" "^7.28.3"
"@babel/helpers" "^7.28.4"
"@babel/parser" "^7.28.5"
"@babel/parser" "^7.28.4"
"@babel/template" "^7.27.2"
"@babel/traverse" "^7.28.5"
"@babel/types" "^7.28.5"
"@babel/traverse" "^7.28.4"
"@babel/types" "^7.28.4"
"@jridgewell/remapping" "^2.3.5"
convert-source-map "^2.0.0"
debug "^4.1.0"
@ -62,13 +62,13 @@
json5 "^2.2.3"
semver "^6.3.1"
"@babel/generator@^7.28.5":
version "7.28.5"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.5.tgz#712722d5e50f44d07bc7ac9fe84438742dd61298"
integrity sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==
"@babel/generator@^7.28.3":
version "7.28.3"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e"
integrity sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==
dependencies:
"@babel/parser" "^7.28.5"
"@babel/types" "^7.28.5"
"@babel/parser" "^7.28.3"
"@babel/types" "^7.28.2"
"@jridgewell/gen-mapping" "^0.3.12"
"@jridgewell/trace-mapping" "^0.3.28"
jsesc "^3.0.2"
@ -91,17 +91,17 @@
lru-cache "^5.1.1"
semver "^6.3.1"
"@babel/helper-create-class-features-plugin@^7.27.1", "@babel/helper-create-class-features-plugin@^7.28.5":
version "7.28.5"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz#472d0c28028850968979ad89f173594a6995da46"
integrity sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==
"@babel/helper-create-class-features-plugin@^7.27.1":
version "7.28.3"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz#3e747434ea007910c320c4d39a6b46f20f371d46"
integrity sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==
dependencies:
"@babel/helper-annotate-as-pure" "^7.27.3"
"@babel/helper-member-expression-to-functions" "^7.28.5"
"@babel/helper-member-expression-to-functions" "^7.27.1"
"@babel/helper-optimise-call-expression" "^7.27.1"
"@babel/helper-replace-supers" "^7.27.1"
"@babel/helper-skip-transparent-expression-wrappers" "^7.27.1"
"@babel/traverse" "^7.28.5"
"@babel/traverse" "^7.28.3"
semver "^6.3.1"
"@babel/helper-globals@^7.28.0":
@ -109,13 +109,13 @@
resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674"
integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==
"@babel/helper-member-expression-to-functions@^7.27.1", "@babel/helper-member-expression-to-functions@^7.28.5":
version "7.28.5"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz#f3e07a10be37ed7a63461c63e6929575945a6150"
integrity sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==
"@babel/helper-member-expression-to-functions@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz#ea1211276be93e798ce19037da6f06fbb994fa44"
integrity sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==
dependencies:
"@babel/traverse" "^7.28.5"
"@babel/types" "^7.28.5"
"@babel/traverse" "^7.27.1"
"@babel/types" "^7.27.1"
"@babel/helper-module-imports@^7.27.1":
version "7.27.1"
@ -168,10 +168,10 @@
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
"@babel/helper-validator-identifier@^7.27.1", "@babel/helper-validator-identifier@^7.28.5":
version "7.28.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4"
integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==
"@babel/helper-validator-identifier@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
"@babel/helper-validator-option@^7.27.1":
version "7.27.1"
@ -186,12 +186,12 @@
"@babel/template" "^7.27.2"
"@babel/types" "^7.28.4"
"@babel/parser@^7.27.2", "@babel/parser@^7.28.0", "@babel/parser@^7.28.4", "@babel/parser@^7.28.5":
version "7.28.5"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.5.tgz#0b0225ee90362f030efd644e8034c99468893b08"
integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==
"@babel/parser@^7.27.2", "@babel/parser@^7.28.0", "@babel/parser@^7.28.3", "@babel/parser@^7.28.4":
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.4.tgz#da25d4643532890932cc03f7705fe19637e03fa8"
integrity sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==
dependencies:
"@babel/types" "^7.28.5"
"@babel/types" "^7.28.4"
"@babel/plugin-proposal-decorators@^7.23.0":
version "7.28.0"
@ -238,12 +238,12 @@
"@babel/helper-plugin-utils" "^7.27.1"
"@babel/plugin-transform-typescript@^7.22.15", "@babel/plugin-transform-typescript@^7.23.3":
version "7.28.5"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz#441c5f9a4a1315039516c6c612fc66d5f4594e72"
integrity sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==
version "7.28.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz#796cbd249ab56c18168b49e3e1d341b72af04a6b"
integrity sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==
dependencies:
"@babel/helper-annotate-as-pure" "^7.27.3"
"@babel/helper-create-class-features-plugin" "^7.28.5"
"@babel/helper-create-class-features-plugin" "^7.27.1"
"@babel/helper-plugin-utils" "^7.27.1"
"@babel/helper-skip-transparent-expression-wrappers" "^7.27.1"
"@babel/plugin-syntax-typescript" "^7.27.1"
@ -262,26 +262,26 @@
"@babel/parser" "^7.27.2"
"@babel/types" "^7.27.1"
"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.5":
version "7.28.5"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.5.tgz#450cab9135d21a7a2ca9d2d35aa05c20e68c360b"
integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==
"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.4":
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.4.tgz#8d456101b96ab175d487249f60680221692b958b"
integrity sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==
dependencies:
"@babel/code-frame" "^7.27.1"
"@babel/generator" "^7.28.5"
"@babel/generator" "^7.28.3"
"@babel/helper-globals" "^7.28.0"
"@babel/parser" "^7.28.5"
"@babel/parser" "^7.28.4"
"@babel/template" "^7.27.2"
"@babel/types" "^7.28.5"
"@babel/types" "^7.28.4"
debug "^4.3.1"
"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.4", "@babel/types@^7.28.5":
version "7.28.5"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b"
integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==
"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.4":
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.4.tgz#0a4e618f4c60a7cd6c11cb2d48060e4dbe38ac3a"
integrity sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==
dependencies:
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.28.5"
"@babel/helper-validator-identifier" "^7.27.1"
"@ctrl/tinycolor@^3.4.0", "@ctrl/tinycolor@^3.5.0":
version "3.6.1"
@ -421,9 +421,9 @@
eslint-visitor-keys "^3.4.3"
"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1":
version "4.12.2"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b"
integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==
version "4.12.1"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0"
integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==
"@eslint/eslintrc@^2.1.4":
version "2.1.4"
@ -498,9 +498,6 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@neptune/wasm@file:./packages/neptune-wasm":
version "0.1.0"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@ -741,9 +738,9 @@
integrity sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==
"@rushstack/eslint-patch@^1.8.0":
version "1.14.1"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.14.1.tgz#5f7c5c335643cff62ad8e6a9432d708a9c51e98c"
integrity sha512-jGTk8UD/RdjsNZW8qq10r0RBvxL8OWtoT+kImlzPDFilmozzM+9QmIJsmze9UiSBrFU45ZxhTYBypn9q9z/VfQ==
version "1.14.0"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.14.0.tgz#61b05741089552a3d352b3503d6a242978a2cb08"
integrity sha512-WJFej426qe4RWOm9MMtP4V3CV4AucXolQty+GRgAWLgQXmpCuwzs7hEpxxhSc/znXUSxum9d/P/32MW0FlAAlA==
"@sec-ant/readable-stream@^0.4.1":
version "0.4.1"
@ -774,9 +771,9 @@
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@types/node@^20.12.5":
version "20.19.23"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.19.23.tgz#7de99389c814071cca78656a3243f224fed7453d"
integrity sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==
version "20.19.22"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.19.22.tgz#f17e80ee1d1fdd10d50bef449abe23bfc0216780"
integrity sha512-hRnu+5qggKDSyWHlnmThnUqg62l29Aj/6vcYgUaSFL9oc7DVjeWEQN3PRgdSc6F8d9QRMWkf36CLMch1Do/+RQ==
dependencies:
undici-types "~6.21.0"
@ -1202,10 +1199,10 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
baseline-browser-mapping@^2.8.19:
version "2.8.20"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz#6766cf270f3668d20b6712b9c54cc911b87da714"
integrity sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==
baseline-browser-mapping@^2.8.9:
version "2.8.18"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.18.tgz#b44b18cadddfa037ee8440dafaba4a329dfb327c"
integrity sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==
birpc@^2.3.0:
version "2.6.1"
@ -1240,15 +1237,15 @@ braces@^3.0.3:
fill-range "^7.1.1"
browserslist@^4.24.0:
version "4.27.0"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.27.0.tgz#755654744feae978fbb123718b2f139bc0fa6697"
integrity sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==
version "4.26.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.26.3.tgz#40fbfe2d1cd420281ce5b1caa8840049c79afb56"
integrity sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==
dependencies:
baseline-browser-mapping "^2.8.19"
caniuse-lite "^1.0.30001751"
electron-to-chromium "^1.5.238"
node-releases "^2.0.26"
update-browserslist-db "^1.1.4"
baseline-browser-mapping "^2.8.9"
caniuse-lite "^1.0.30001746"
electron-to-chromium "^1.5.227"
node-releases "^2.0.21"
update-browserslist-db "^1.1.3"
bundle-name@^4.1.0:
version "4.1.0"
@ -1270,7 +1267,7 @@ callsites@^3.0.0:
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
caniuse-lite@^1.0.30001751:
caniuse-lite@^1.0.30001746:
version "1.0.30001751"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz#dacd5d9f4baeea841641640139d2b2a4df4226ad"
integrity sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==
@ -1324,12 +1321,12 @@ convert-source-map@^2.0.0:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
copy-anything@^4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-4.0.5.tgz#16cabafd1ea4bb327a540b750f2b4df522825aea"
integrity sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==
copy-anything@^3.0.2:
version "3.0.5"
resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.5.tgz#2d92dce8c498f790fa7ad16b01a1ae5a45b020a0"
integrity sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==
dependencies:
is-what "^5.2.0"
is-what "^4.1.8"
core-js@^3.15.1:
version "3.46.0"
@ -1438,10 +1435,10 @@ dunder-proto@^1.0.1:
es-errors "^1.3.0"
gopd "^1.2.0"
electron-to-chromium@^1.5.238:
version "1.5.240"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.240.tgz#bfd946570a723aa3754370065d02e23e30824774"
integrity sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ==
electron-to-chromium@^1.5.227:
version "1.5.237"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz#eacf61cef3f6345d0069ab427585c5a04d7084f0"
integrity sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==
entities@^4.5.0:
version "4.5.0"
@ -2006,10 +2003,10 @@ is-unicode-supported@^2.0.0:
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz#09f0ab0de6d3744d48d265ebb98f65d11f2a9b3a"
integrity sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==
is-what@^5.2.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/is-what/-/is-what-5.5.0.tgz#a3031815757cfe1f03fed990bf6355a2d3f628c4"
integrity sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==
is-what@^4.1.8:
version "4.1.16"
resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.16.tgz#1ad860a19da8b4895ad5495da3182ce2acdd7a6f"
integrity sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==
is-wsl@^3.1.0:
version "3.1.0"
@ -2131,9 +2128,9 @@ lru-cache@^5.1.1:
yallist "^3.0.2"
magic-string@^0.30.19, magic-string@^0.30.4:
version "0.30.21"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91"
integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==
version "0.30.19"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.19.tgz#cebe9f104e565602e5d2098c5f2e79a77cc86da9"
integrity sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.5"
@ -2231,10 +2228,10 @@ node-addon-api@^7.0.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558"
integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
node-releases@^2.0.26:
version "2.0.26"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.26.tgz#fdfa272f2718a1309489d18aef4ef5ba7f5dfb52"
integrity sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==
node-releases@^2.0.21:
version "2.0.25"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.25.tgz#95479437bd409231e03981c1f6abee67f5e962df"
integrity sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==
npm-normalize-package-bin@^3.0.0:
version "3.0.1"
@ -2634,11 +2631,11 @@ stylis@^4.1.3:
integrity sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==
superjson@^2.2.2:
version "2.2.3"
resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.3.tgz#c42236fff6ecc449b7ffa7f023a9a028a5ec9c87"
integrity sha512-ay3d+LW/S6yppKoTz3Bq4mG0xrS5bFwfWEBmQfbC7lt5wmtk+Obq0TxVuA9eYRirBTQb1K3eEpBRHMQEo0WyVw==
version "2.2.2"
resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.2.tgz#9d52bf0bf6b5751a3c3472f1292e714782ba3173"
integrity sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==
dependencies:
copy-anything "^4"
copy-anything "^3.0.2"
supports-color@^7.1.0:
version "7.2.0"
@ -2713,10 +2710,10 @@ universalify@^2.0.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
update-browserslist-db@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz#7802aa2ae91477f255b86e0e46dbc787a206ad4a"
integrity sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==
update-browserslist-db@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420"
integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==
dependencies:
escalade "^3.2.0"
picocolors "^1.1.1"