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

View File

@ -1,22 +1,41 @@
import axios from 'axios' import axios from 'axios'
import router from '@/router'
import { STATUS_CODE_SUCCESS, ACCESS_TOKEN, STATUS_CODE_UNAUTHORIZED } from '@/utils'
axios.defaults.withCredentials = false axios.defaults.withCredentials = false
export const API_URL = import.meta.env.VITE_APP_API export const API_URL = import.meta.env.VITE_APP_API
const instance = axios.create({ const instance = axios.create({
baseURL: API_URL, 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( instance.interceptors.response.use(
function (response) { function (response) {
// if (response?.status !== STATUS_CODE_SUCCESS) return Promise.reject(response?.data) if (response?.status !== STATUS_CODE_SUCCESS) return Promise.reject(response?.data)
return response return response.data
}, },
function (error) { function (error) {
if (error?.response?.status === STATUS_CODE_UNAUTHORIZED || error.code === 'ERR_NETWORK') {
localStorage.clear()
return router.push({ name: 'login' })
}
if (error?.response?.data) { if (error?.response?.data) {
return Promise.reject(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"> <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 { 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 availableBalance = ref(0)
const pendingBalance = ref(0) const pendingBalance = ref(0)
const currentDaaScore = ref(0) const receiveAddress = ref('kaspa:qpn80v050r3jxv6mzt8tzss6dhvllc3rvcuuy86z6djgmvzx0napvhuj7ugh9')
const isLoadingData = ref(false) const walletStatus = ref('Online')
const currentDaaScore = ref(255953336)
const receiveAddress = computed(() => neptuneStore.getWallet?.address || '') const copyAddress = () => {
const walletStatus = computed(() => { navigator.clipboard.writeText(receiveAddress.value)
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 handleSend = () => { 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> </script>
<template> <template>
<div class="wallet-info-container"> <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 --> <!-- Balance Section -->
<div class="balance-section"> <div class="balance-section">
<div class="balance-label">Available</div> <div class="balance-label">Available</div>
<div class="balance-amount"> <div class="balance-amount">{{ availableBalance }} KAS</div>
<span v-if="isLoadingData">Loading...</span>
<span v-else>{{ formatNumberToLocaleString(availableBalance) }} NEPT</span>
</div>
<div class="pending-section"> <div class="pending-section">
<span class="pending-label">Pending</span> <span class="pending-label">Pending</span>
<span class="pending-amount"> <span class="pending-amount">{{ pendingBalance }} KAS</span>
{{ isLoadingData ? '...' : formatNumberToLocaleString(pendingBalance) }}
NEPT
</span>
</div> </div>
</div> </div>
@ -110,7 +34,7 @@ onMounted(() => {
<div class="receive-section"> <div class="receive-section">
<div class="address-label">Receive Address:</div> <div class="address-label">Receive Address:</div>
<div class="address-value" @click="copyAddress"> <div class="address-value" @click="copyAddress">
{{ receiveAddress || 'No address available' }} {{ receiveAddress }}
<svg <svg
class="copy-icon" class="copy-icon"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -126,13 +50,7 @@ onMounted(() => {
</div> </div>
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="action-buttons"> <div class="action-buttons">
<ButtonCommon <ButtonCommon type="primary" size="large" block @click="handleSend" class="btn-send">
type="primary"
size="large"
block
@click="handleSend"
class="btn-send"
>
SEND SEND
</ButtonCommon> </ButtonCommon>
</div> </div>
@ -143,13 +61,9 @@ onMounted(() => {
>Wallet Status: <strong>{{ walletStatus }}</strong></span >Wallet Status: <strong>{{ walletStatus }}</strong></span
> >
<span <span
>DAA Score: >DAA score: <strong>{{ formatNumberToLocaleString(currentDaaScore) }}</strong></span
<strong>{{
isLoadingData ? '...' : formatNumberToLocaleString(currentDaaScore)
}}</strong></span
> >
</div> </div>
</template>
</div> </div>
</template> </template>
@ -158,18 +72,6 @@ onMounted(() => {
@include card-base; @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 { .balance-section {
text-align: center; text-align: center;
margin-bottom: var(--spacing-3xl); margin-bottom: var(--spacing-3xl);

View File

@ -1,62 +1,141 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, defineEmits, onMounted, computed } from 'vue' import { ref, defineEmits, onMounted } from 'vue'
import { ButtonCommon } from '@/components' import { ButtonCommon } from '@/components'
import { useNeptuneStore } from '@/stores/neptuneStore' import { useSeedStore } from '@/stores'
import { message } from 'ant-design-vue'
const emit = defineEmits<{ const emit = defineEmits<{
next: [] next: []
back: [] back: []
}>() }>()
const neptuneStore = useNeptuneStore() const seedStore = useSeedStore()
const seedWords = computed(() => neptuneStore.getSeedPhrase || []) const seedWords = ref<string[]>([])
const currentQuestionIndex = ref(0) const currentQuestionIndex = ref(0)
const selectedAnswer = ref('') const selectedAnswer = ref('')
const isCorrect = ref(false) const isCorrect = ref(false)
const showResult = ref(false) const showResult = ref(false)
const correctCount = ref(0)
const totalQuestions = 3
const askedPositions = ref<Set<number>>(new Set())
const generateQuiz = (): { const generateQuiz = (): {
position: number position: number
correctWord: string correctWord: string
options: string[] options: string[]
} | null => { } | null => {
if (!seedWords.value || seedWords.value.length === 0) { if (seedWords.value.length === 0) return null
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))
const randomPosition = Math.floor(Math.random() * 12) + 1
currentQuestionIndex.value = randomPosition - 1 currentQuestionIndex.value = randomPosition - 1
const correctWord = seedWords.value[randomPosition - 1] const correctWord = seedWords.value[randomPosition - 1]
const options = [correctWord] const options = [correctWord]
const otherWords = seedWords.value.filter((_, index) => index !== randomPosition - 1) const BIP39_WORDS = [
'abandon',
while (options.length < 4 && otherWords.length > 0) { 'ability',
const randomIndex = Math.floor(Math.random() * otherWords.length) 'able',
const randomWord = otherWords[randomIndex] '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)) { if (!options.includes(randomWord)) {
options.push(randomWord) options.push(randomWord)
otherWords.splice(randomIndex, 1)
} }
} }
@ -83,10 +162,6 @@ const handleAnswerSelect = (answer: string) => {
const handleNext = () => { const handleNext = () => {
if (isCorrect.value) { if (isCorrect.value) {
correctCount.value++
askedPositions.value.add(quizData.value!.position)
if (correctCount.value >= totalQuestions) {
emit('next') emit('next')
} else { } else {
showResult.value = false showResult.value = false
@ -96,32 +171,37 @@ const handleNext = () => {
quizData.value = newQuiz quizData.value = newQuiz
} }
} }
} else {
showResult.value = false
selectedAnswer.value = ''
const newQuiz = generateQuiz()
if (newQuiz) {
quizData.value = newQuiz
}
}
}
const handleBack = () => {
emit('back')
} }
onMounted(() => { onMounted(() => {
const seeds = neptuneStore.getSeedPhrase const words = seedStore.getSeedWords()
if (words.length > 0) {
if (!seeds || seeds.length === 0) { seedWords.value = words
message.warning('No seed phrase found. Please go back and generate a wallet first.')
return
}
const newQuiz = generateQuiz() const newQuiz = generateQuiz()
if (newQuiz) { if (newQuiz) {
quizData.value = 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> </script>
@ -144,9 +224,6 @@ onMounted(() => {
Make sure you wrote the phrase down correctly by answering this quick Make sure you wrote the phrase down correctly by answering this quick
checkup. checkup.
</p> </p>
<p class="progress-text">
Question {{ correctCount + 1 }} / {{ totalQuestions }}
</p>
</div> </div>
<div class="quiz-section"> <div class="quiz-section">
@ -173,42 +250,14 @@ onMounted(() => {
</div> </div>
<div v-if="showResult" class="result-message"> <div v-if="showResult" class="result-message">
<p <p v-if="isCorrect" class="success-message"> Correct! You can proceed.</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-else class="error-message"> Incorrect. Please try again.</p> <p v-else class="error-message"> Incorrect. Please try again.</p>
</div> </div>
</div> </div>
<div class="confirm-actions"> <div class="confirm-actions">
<ButtonCommon <ButtonCommon
v-if=" v-if="showResult && isCorrect"
!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"
type="primary" type="primary"
size="large" size="large"
@click="handleNext" @click="handleNext"
@ -282,13 +331,6 @@ onMounted(() => {
line-height: var(--leading-normal); line-height: var(--leading-normal);
margin: 0; 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 { .quiz-section {
@ -369,14 +411,10 @@ onMounted(() => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
gap: var(--spacing-md); gap: var(--spacing-md);
&:has(:only-child) {
justify-content: flex-end;
}
} }
} }
@include screen(mobile) { @media (max-width: 640px) {
.confirm-container { .confirm-container {
padding: var(--spacing-md); padding: var(--spacing-md);
} }
@ -385,7 +423,6 @@ onMounted(() => {
max-width: 100%; max-width: 100%;
} }
.confirm-content {
.quiz-section { .quiz-section {
.answer-options { .answer-options {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@ -396,5 +433,4 @@ onMounted(() => {
flex-direction: column; flex-direction: column;
} }
} }
}
</style> </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<{ const emit = defineEmits<{
( (
e: 'import-success', e: 'import-success',
data: { type: 'seed' | 'keystore'; value: string | string[]; passphrase?: string } data: { type: 'seed' | 'privatekey'; value: string | string[]; passphrase?: string }
): void ): void
}>() }>()
const tab = ref<'seedphrase' | 'keystore'>('seedphrase') const tab = ref<'seedphrase' | 'privatekey'>('seedphrase')
const seedWords = ref<string[]>(Array(18).fill('')) const seedWords = ref<string[]>(Array(18).fill(''))
const seedError = ref('') const seedError = ref('')
const passphrase = ref('') const passphrase = ref('')
const keystore = ref('') const privateKey = ref('')
const keystoreError = ref('') const privateKeyError = ref('')
const inputBoxFocus = (idx: number) => { const inputBoxFocus = (idx: number) => {
document.getElementById('input-' + idx)?.focus() document.getElementById('input-' + idx)?.focus()
@ -34,12 +34,12 @@ const validateSeed = () => {
return true return true
} }
const validateKeystore = () => { const validateKey = () => {
if (!keystore.value.trim()) { if (!privateKey.value.trim()) {
keystoreError.value = 'Please enter your keystore.' privateKeyError.value = 'Please enter your private key.'
return false return false
} }
keystoreError.value = '' privateKeyError.value = ''
return true return true
} }
@ -53,8 +53,8 @@ const handleContinue = () => {
}) })
} }
} else { } else {
if (validateKeystore()) { if (validateKey()) {
emit('import-success', { type: 'keystore', value: keystore.value }) emit('import-success', { type: 'privatekey', value: privateKey.value })
} }
} }
} }
@ -70,8 +70,11 @@ const handleContinue = () => {
> >
Import by seed phrase Import by seed phrase
</button> </button>
<button :class="['tab-btn', tab === 'keystore' && 'active']" @click="tab = 'keystore'"> <button
Import by keystore :class="['tab-btn', tab === 'privatekey' && 'active']"
@click="tab = 'privatekey'"
>
Import by private key
</button> </button>
</div> </div>
<div v-if="tab === 'seedphrase'" class="tab-pane"> <div v-if="tab === 'seedphrase'" class="tab-pane">
@ -110,12 +113,12 @@ const handleContinue = () => {
<div v-else class="tab-pane"> <div v-else class="tab-pane">
<div class="form-row mb-md"> <div class="form-row mb-md">
<FormCommon <FormCommon
v-model="keystore" v-model="privateKey"
type="text" type="text"
label="Keystore" label="Private key"
placeholder="Enter keystore" placeholder="Enter private key"
:error="keystoreError" :error="privateKeyError"
@focus="keystoreError = ''" @focus="privateKeyError = ''"
/> />
</div> </div>
</div> </div>
@ -127,7 +130,7 @@ const handleContinue = () => {
:disabled=" :disabled="
tab === 'seedphrase' tab === 'seedphrase'
? !seedWords.every((w) => w) || !!seedError ? !seedWords.every((w) => w) || !!seedError
: !keystore || !!keystoreError : !privateKey || !!privateKeyError
" "
@click="handleContinue" @click="handleContinue"
>Continue</ButtonCommon >Continue</ButtonCommon
@ -255,7 +258,7 @@ const handleContinue = () => {
margin-top: 3px; margin-top: 3px;
} }
} }
@include screen(mobile) { @media (max-width: 600px) {
.import-wallet { .import-wallet {
padding: 16px 5px; padding: 16px 5px;
} }

View File

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

View File

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

View File

@ -1,44 +1,31 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineEmits, computed } from 'vue' import { ref, defineEmits, onMounted } from 'vue'
import { ButtonCommon } from '@/components' import { ButtonCommon } from '@/components'
import { useNeptuneStore } from '@/stores/neptuneStore' import { generateSeedPhrase } from '@/utils'
import { message } from 'ant-design-vue' import { useSeedStore } from '@/stores'
const emit = defineEmits<{ const emit = defineEmits<{
next: [] next: []
back: [] 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 = () => { const handleNext = () => {
if (!seedWords.value || seedWords.value.length === 0) {
message.error('❌ Cannot proceed without seed phrase')
return
}
emit('next') emit('next')
} }
const handleBack = () => { const handleBack = () => {
emit('back') 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> </script>
<template> <template>
@ -52,49 +39,27 @@ const handleCopySeed = async () => {
<div class="instruction-text"> <div class="instruction-text">
<p> <p>
Your wallet is accessible by a seed phrase. The seed phrase is an ordered Your wallet is accessible by a seed phrase. The seed phrase is an ordered
18-word secret phrase. 12-word secret phrase.
</p> </p>
<p> <p>
Make sure no one is looking, as anyone with your seed phrase can access your 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> </p>
</div> </div>
<div v-if="seedWords.length > 0" class="seed-words-container"> <div class="seed-words-container">
<div class="seed-words-grid"> <div class="seed-words-grid">
<div v-for="(word, index) in seedWords" :key="index" class="seed-word-item"> <div v-for="(word, index) in seedWords" :key="index" class="seed-word-item">
<span class="word-number">{{ index + 1 }}</span> <span class="word-number">{{ index + 1 }}</span>
<span class="word-text">{{ word }}</span> <span class="word-text">{{ word }}</span>
</div> </div>
</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>
<div class="cool-fact"> <div class="cool-fact">
<p> <p>
Cool fact: there are more 18-word phrase combinations than atoms in the Cool fact: there are more 12-word phrase combinations than nanoseconds since
observable universe! the big bang!
</p> </p>
</div> </div>
@ -102,12 +67,7 @@ const handleCopySeed = async () => {
<ButtonCommon type="default" size="large" @click="handleBack"> <ButtonCommon type="default" size="large" @click="handleBack">
BACK BACK
</ButtonCommon> </ButtonCommon>
<ButtonCommon <ButtonCommon type="primary" size="large" @click="handleNext">
type="primary"
size="large"
@click="handleNext"
:disabled="!seedWords || seedWords.length === 0"
>
NEXT NEXT
</ButtonCommon> </ButtonCommon>
</div> </div>
@ -174,7 +134,6 @@ const handleCopySeed = async () => {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
gap: var(--spacing-md); gap: var(--spacing-md);
margin-bottom: var(--spacing-md);
.seed-word-item { .seed-word-item {
display: flex; 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 { .cool-fact {
@ -249,25 +177,10 @@ const handleCopySeed = async () => {
justify-content: space-between; justify-content: space-between;
gap: var(--spacing-md); 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 { .recovery-container {
padding: var(--spacing-md); padding: var(--spacing-md);
} }
@ -276,7 +189,6 @@ const handleCopySeed = async () => {
max-width: 100%; max-width: 100%;
} }
.recovery-content {
.seed-words-container { .seed-words-container {
.seed-words-grid { .seed-words-grid {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
@ -292,5 +204,4 @@ const handleCopySeed = async () => {
} }
} }
} }
}
</style> </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 LayoutVue from './common/LayoutVue.vue'
import ButtonCommon from './common/ButtonCommon.vue' import ButtonCommon from './common/ButtonCommon.vue'
import FormCommon from './common/FormCommon.vue' import FormCommon from './common/FormCommon.vue'
import 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' 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 './common'
export * from './home' 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 * as Page from '@/views'
import { useNeptuneStore } from '@/stores/neptuneStore' import { getToken } from '@/utils'
export const ifAuthenticated = (to: any, from: any, next: any) => { const ifAuthenticated = (to: any, from: any, next: any) => {
const neptuneStore = useNeptuneStore() if (getToken()) {
if (neptuneStore.getReceiverId) {
next() next()
return return
} }
next('/login') next('/login')
} }
const ifNotAuthenticated = (to: any, from: any, next: any) => {
if (!getToken()) {
next()
return
}
next('/')
}
export const routes: any = [ export const routes: any = [
{ {
path: '/', path: '/',
name: 'home', name: 'index',
component: Page.Home, component: Page.Home,
beforeEnter: ifAuthenticated,
}, },
{ {
path: '/login', path: '/login',
name: 'login', name: 'login',
component: Page.Auth, 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(.*)*', path: '/:pathMatch(.*)*',

View File

@ -1,3 +1,2 @@
export * from './seedStore' export * from './seedStore'
export * from './authStore' 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 if (words.length !== 18) return false
return words.every((word) => BIP39_WORDS.includes(word.toLowerCase())) return words.every((word) => BIP39_WORDS.includes(word.toLowerCase()))
} }
export const generateSeedPhrase18 = (): string[] => {
const words: string[] = []
for (let i = 0; i < 18; i++) {
const randomIndex = Math.floor(Math.random() * BIP39_WORDS.length)
words.push(BIP39_WORDS[randomIndex])
}
return words
}

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { CreateWalletComponent } from '.' import { CreateWalletComponent } from '@/components'
const emit = defineEmits<{ const emit = defineEmits<{
goToLogin: [] 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"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { ImportWalletComponent, OpenWalletComponent } from '.' import { ImportWalletComponent, OpenWalletComponent } from '@/components'
const emit = defineEmits<{ goToCreate: [] }>() const emit = defineEmits<{ goToCreate: [] }>()

View File

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

View File

@ -1,17 +1,4 @@
// Tabs
export { default as LoginTab } from './LoginTab.vue' export { default as LoginTab } from './LoginTab.vue'
export { default as CreateTab } from './CreateTab.vue' export { default as CreateTab } from './CreateTab.vue'
export { default as RecoveryTab } from './RecoveryTab.vue' export { default as RecoveryTab } from './RecoveryTab.vue'
export { default as ConfirmTab } from './ConfirmTab.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); padding: var(--spacing-lg);
font-family: var(--font-primary); font-family: var(--font-primary);
@include screen(tablet) { @media (min-width: 768px) {
padding: var(--spacing-2xl);
}
@include screen(desktop) {
padding: var(--spacing-2xl); padding: var(--spacing-2xl);
} }
} }
@ -66,7 +62,7 @@ const network = ref('kaspa-mainnet')
letter-spacing: var(--tracking-wide); letter-spacing: var(--tracking-wide);
padding: 10px 16px; padding: 10px 16px;
@include screen(mobile) { @media (max-width: 768px) {
font-size: 12px; font-size: 12px;
padding: 8px 12px; padding: 8px 12px;
} }

View File

@ -1,83 +1,111 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { formatNumberToLocaleString } from '@/utils' import { formatNumberToLocaleString } from '@/utils'
import { SpinnerCommon } from '@/components' import type { NetworkStatus } from '@/interface'
import { useNeptuneStore } from '@/stores/neptuneStore'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { message } from 'ant-design-vue'
const neptuneStore = useNeptuneStore() const networkStatus = ref<NetworkStatus>({
const { getBlockHeight, getNetworkInfo } = useNeptuneWallet() network: 'kaspa-mainnet',
daaScore: 0,
const blockHeight = ref(0) dagHeader: 0,
const networkInfo = ref<any>(null) dagBlocks: 0,
const loading = ref(true) difficulty: 0,
const error = ref('') medianOffset: '00:00:00',
const lastUpdate = ref<Date | null>(null) medianTimeUTC: '',
// 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()
}) })
// onUnmounted(() => { const loading = ref(true)
// stopPolling() 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> </script>
<template> <template>
@ -86,8 +114,8 @@ onMounted(async () => {
<h2 class="section-title">NETWORK STATUS</h2> <h2 class="section-title">NETWORK STATUS</h2>
<!-- Loading State --> <!-- Loading State -->
<div v-if="loading && !blockHeight" class="loading-state"> <div v-if="loading && networkStatus.daaScore === 0" class="loading-state">
<SpinnerCommon size="medium" /> <div class="spinner"></div>
<p>Loading network data...</p> <p>Loading network data...</p>
</div> </div>
@ -101,32 +129,53 @@ onMounted(async () => {
<div v-else class="status-grid"> <div v-else class="status-grid">
<div class="status-item"> <div class="status-item">
<span class="status-label">Network</span> <span class="status-label">Network</span>
<span class="status-value">Neptune {{ network }}</span> <span class="status-value">{{ networkStatus.network }}</span>
</div> </div>
<div class="status-item"> <div class="status-item">
<span class="status-label">Block Height</span> <span class="status-label">DAA Score</span>
<span class="status-value">{{ formatNumberToLocaleString(blockHeight) }}</span> <span class="status-value">{{
formatNumberToLocaleString(networkStatus.daaScore)
}}</span>
</div> </div>
<div v-if="networkInfo?.version" class="status-item"> <div class="status-item">
<span class="status-label">Version</span> <span class="status-label">DAG Header</span>
<span class="status-value">{{ networkInfo.version }}</span> <span class="status-value">{{
formatNumberToLocaleString(networkStatus.dagHeader)
}}</span>
</div> </div>
<div v-if="networkInfo?.peer_count !== undefined" class="status-item"> <div class="status-item">
<span class="status-label">Peers</span> <span class="status-label">DAG Blocks</span>
<span class="status-value">{{ networkInfo.peer_count }}</span> <span class="status-value">{{
formatNumberToLocaleString(networkStatus.dagBlocks)
}}</span>
</div> </div>
<div v-if="networkInfo?.chain_id" class="status-item"> <div class="status-item">
<span class="status-label">Chain ID</span> <span class="status-label">Difficulty</span>
<span class="status-value">{{ networkInfo.chain_id }}</span> <span class="status-value">{{
formatNumberToLocaleString(networkStatus.difficulty)
}}</span>
</div> </div>
<div v-if="lastUpdate" class="status-item"> <div class="status-item">
<span class="status-label">Last Updated</span> <span class="status-label">Median Offset</span>
<span class="status-value">{{ lastUpdate.toLocaleTimeString() }}</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> </div>
</div> </div>
@ -187,14 +236,29 @@ onMounted(async () => {
// Loading State // Loading State
.loading-state { .loading-state {
@include center_flex; text-align: center;
padding: var(--spacing-4xl); padding: var(--spacing-4xl);
color: var(--text-secondary); 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
.error-state { .error-state {
@include center_flex; text-align: center;
padding: var(--spacing-4xl); padding: var(--spacing-4xl);
color: var(--error-color); 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> </style>

View File

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

View File

@ -1,20 +1,36 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'
import { Divider } from 'ant-design-vue' import { Divider } from 'ant-design-vue'
import ButtonCommon from '@/components/common/ButtonCommon.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 = () => { const handleBackupFile = () => {
// TODO: Implement backup file functionality console.log('Backup File')
} }
const handleBackupSeed = () => { const handleBackupSeed = () => {
// TODO: Implement backup seed functionality console.log('Backup Seed')
} }
</script> </script>
<template> <template>
<div class="content-card wallet-info-card"> <div class="content-card wallet-info-card">
<div class="wallet-header"> <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>
<div class="wallet-actions"> <div class="wallet-actions">

View File

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

209
yarn.lock
View File

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