Compare commits
2 Commits
3040bd5a57
...
065b77db84
| Author | SHA1 | Date | |
|---|---|---|---|
| 065b77db84 | |||
| ecbcc08209 |
@ -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",
|
||||
|
||||
@ -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
53
src/api/neptuneApi.ts
Normal 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
14
src/api/request.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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ả lập nội dung keystore (có thể tuỳ chỉnh)
|
||||
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>
|
||||
52
src/components/common/SpinnerCommon.vue
Normal file
52
src/components/common/SpinnerCommon.vue
Normal 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>
|
||||
@ -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 }
|
||||
|
||||
292
src/composables/useNeptuneWallet.ts
Normal file
292
src/composables/useNeptuneWallet.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@ -1,2 +1,3 @@
|
||||
export * from './common'
|
||||
export * from './home'
|
||||
export * from './neptune'
|
||||
|
||||
20
src/interface/neptune.ts
Normal file
20
src/interface/neptune.ts
Normal 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
|
||||
}
|
||||
@ -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(.*)*',
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from './seedStore'
|
||||
export * from './authStore'
|
||||
export * from './neptuneStore'
|
||||
|
||||
113
src/stores/neptuneStore.ts
Normal file
113
src/stores/neptuneStore.ts
Normal 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
39
src/types/neptune-wasm.d.ts
vendored
Normal 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 {}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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>
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ConfirmSeedComponent } from '@/components'
|
||||
import { ConfirmSeedComponent } from '.'
|
||||
|
||||
const emit = defineEmits<{
|
||||
next: []
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { CreateWalletComponent } from '@/components'
|
||||
import { CreateWalletComponent } from '.'
|
||||
|
||||
const emit = defineEmits<{
|
||||
goToLogin: []
|
||||
|
||||
176
src/views/Auth/components/CreateWalletComponent.vue
Normal file
176
src/views/Auth/components/CreateWalletComponent.vue
Normal 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>
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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: [] }>()
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { RecoverySeedComponent } from '@/components'
|
||||
import { RecoverySeedComponent } from '.'
|
||||
|
||||
const emit = defineEmits<{
|
||||
next: []
|
||||
|
||||
@ -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'
|
||||
|
||||
46
src/views/Auth/components/steps/ChooseBackupMethodStep.vue
Normal file
46
src/views/Auth/components/steps/ChooseBackupMethodStep.vue
Normal 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>
|
||||
384
src/views/Auth/components/steps/CreatePasswordStep.vue
Normal file
384
src/views/Auth/components/steps/CreatePasswordStep.vue
Normal 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>
|
||||
125
src/views/Auth/components/steps/WalletCreatedStep.vue
Normal file
125
src/views/Auth/components/steps/WalletCreatedStep.vue
Normal 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>
|
||||
3
src/views/Auth/components/steps/index.ts
Normal file
3
src/views/Auth/components/steps/index.ts
Normal 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'
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -9,6 +9,9 @@ export default defineConfig({
|
||||
port: 3008,
|
||||
},
|
||||
plugins: [vue(), vueJsx(), VueDevTools()],
|
||||
optimizeDeps: {
|
||||
exclude: ['@neptune/wasm'],
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
|
||||
209
yarn.lock
209
yarn.lock
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user