Compare commits

...

2 Commits

Author SHA1 Message Date
065b77db84 feat: 241025/login_by_seed_phrase 2025-10-24 22:50:52 +07:00
ecbcc08209 feat: keystore and recovery seed 2025-10-24 22:50:52 +07:00
39 changed files with 1970 additions and 1191 deletions

View File

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

View File

@ -1,41 +1,22 @@
import axios from 'axios'
import router from '@/router'
import { STATUS_CODE_SUCCESS, ACCESS_TOKEN, STATUS_CODE_UNAUTHORIZED } from '@/utils'
axios.defaults.withCredentials = false
export const API_URL = import.meta.env.VITE_APP_API
const instance = axios.create({
baseURL: API_URL,
})
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
headers: {
'Content-Type': 'application/json',
},
function (error) {
return Promise.reject(error)
}
)
method: 'POST',
})
instance.interceptors.response.use(
function (response) {
if (response?.status !== STATUS_CODE_SUCCESS) return Promise.reject(response?.data)
return response.data
// if (response?.status !== STATUS_CODE_SUCCESS) return Promise.reject(response?.data)
return response
},
function (error) {
if (error?.response?.status === STATUS_CODE_UNAUTHORIZED || error.code === 'ERR_NETWORK') {
localStorage.clear()
return router.push({ name: 'login' })
}
if (error?.response?.data) {
return Promise.reject(error?.response?.data)
}

53
src/api/neptuneApi.ts Normal file
View File

@ -0,0 +1,53 @@
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', [])
}

14
src/api/request.ts Normal file
View File

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

View File

@ -1,561 +0,0 @@
<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

@ -0,0 +1,52 @@
<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,25 +1,7 @@
import LayoutVue from './common/LayoutVue.vue'
import ButtonCommon from './common/ButtonCommon.vue'
import FormCommon from './common/FormCommon.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 SpinnerCommon from './common/SpinnerCommon.vue'
import { IconCommon } from './icon'
export {
LayoutVue,
ButtonCommon,
FormCommon,
OnboardingComponent,
OpenWalletComponent,
CreateWalletComponent,
RecoverySeedComponent,
ConfirmSeedComponent,
IconCommon,
ImportWalletComponent,
KeystoreDownloadComponent,
}
export { LayoutVue, ButtonCommon, FormCommon, SpinnerCommon, IconCommon }

View File

@ -0,0 +1,292 @@
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,2 +1,3 @@
export * from './common'
export * from './home'
export * from './neptune'

20
src/interface/neptune.ts Normal file
View File

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

View File

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

113
src/stores/neptuneStore.ts Normal file
View File

@ -0,0 +1,113 @@
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,
}
})

39
src/types/neptune-wasm.d.ts vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,176 @@
<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

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

View File

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

View File

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

View File

@ -188,7 +188,7 @@ const navigateToNewWallet = () => {
max-width: 420px;
width: 100%;
@media (max-width: 640px) {
@include screen(mobile) {
max-width: 100%;
}
}
@ -312,27 +312,21 @@ const navigateToNewWallet = () => {
}
// Responsive Design
@media (max-width: 640px) {
@include screen(mobile) {
.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);
}
}
.logo-container {
.logo-circle {
width: 40px;
height: 40px;
}
.auth-title {
font-size: var(--font-xl);
.logo-text {
.coin-name {
font-size: var(--font-md);
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,46 @@
<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

@ -0,0 +1,384 @@
<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

@ -0,0 +1,125 @@
<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

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

View File

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

View File

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

View File

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

View File

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

View File

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

209
yarn.lock
View File

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