Compare commits

..

1 Commits

Author SHA1 Message Date
035e4cd1b4 optimize scan UTXO 2025-11-10 15:50:35 +07:00
26 changed files with 115 additions and 87 deletions

View File

@ -78,7 +78,7 @@ ipcMain.handle('wallet:checkKeystore', async () => {
fs.statSync(path.join(walletDir, a)).mtime.getTime()
)[0]
if (!newestFile?.length) return { exists: false, filePath: null }
if (!newestFile) return { exists: false, filePath: null }
const resolvedPath = path.join(walletDir, newestFile)
let minBlockHeight: number | null = null
@ -88,10 +88,11 @@ ipcMain.handle('wallet:checkKeystore', async () => {
const data = JSON.parse(json)
const height = data?.minBlockHeight
if (Number.isFinite(height)) minBlockHeight = height
if (typeof height === 'number' && Number.isFinite(height)) {
minBlockHeight = height
}
} catch (error) {
console.warn('Unable to read minBlockHeight from keystore:', error)
return { exists: true, filePath: resolvedPath }
}
return { exists: true, filePath: resolvedPath, minBlockHeight }
@ -136,34 +137,37 @@ ipcMain.handle(
}
)
ipcMain.handle('wallet:getMinBlockHeight', async (_event, filePath: string | null) => {
if (!filePath) {
return { success: false, error: 'No keystore file path provided.', minBlockHeight: null }
}
try {
const normalizedPath = path.isAbsolute(filePath)
? filePath
: path.join(process.cwd(), filePath)
if (!fs.existsSync(normalizedPath)) {
return { success: false, error: 'Keystore file not found.', minBlockHeight: null }
ipcMain.handle(
'wallet:getMinBlockHeight',
async (_event, filePath: string | null) => {
if (!filePath) {
return { success: false, error: 'No keystore file path provided.', minBlockHeight: null }
}
const fileContents = fs.readFileSync(normalizedPath, 'utf-8')
const walletJson = JSON.parse(fileContents)
const height = walletJson?.minBlockHeight
try {
const normalizedPath = path.isAbsolute(filePath)
? filePath
: path.join(process.cwd(), filePath)
if (Number.isFinite(height)) {
return { success: true, minBlockHeight: height }
if (!fs.existsSync(normalizedPath)) {
return { success: false, error: 'Keystore file not found.', minBlockHeight: null }
}
const fileContents = fs.readFileSync(normalizedPath, 'utf-8')
const walletJson = JSON.parse(fileContents)
const height = walletJson?.minBlockHeight
if (typeof height === 'number' && Number.isFinite(height)) {
return { success: true, minBlockHeight: height }
}
return { success: true, minBlockHeight: null }
} catch (error) {
console.error('Error reading min block height:', error)
return { success: false, error: String(error), minBlockHeight: null }
}
return { success: true, minBlockHeight: null }
} catch (error) {
console.error('Error reading min block height:', error)
return { success: false, error: String(error), minBlockHeight: null }
}
})
)
ipcMain.handle('wallet:generateKeysFromSeed', async (_event, seedPhrase: string[]) => {
try {
@ -184,6 +188,7 @@ ipcMain.handle('wallet:buildTransaction', async (_event, args) => {
import.meta.env.VITE_APP_API,
spendingKeyHex,
inputAdditionRecords,
// pass minBlockHeight from args if provided, default 0
typeof args?.minBlockHeight === 'number' && Number.isFinite(args.minBlockHeight)
? args.minBlockHeight
: 0,

View File

@ -4,6 +4,7 @@ export async function encrypt(seed: string, password: string) {
const salt = crypto.randomBytes(16)
const iv = crypto.randomBytes(12)
// derive 32-byte key từ password
const key = await new Promise<Buffer>((resolve, reject) => {
crypto.scrypt(password, salt, 32, { N: 16384, r: 8, p: 1 }, (err, derivedKey) => {
if (err) reject(err)

Binary file not shown.

View File

@ -1,4 +1,6 @@
<script setup lang="ts">
import { LayoutVue } from '@/components'
const config = {
token: {
colorPrimary: '#42A5F5',

View File

@ -2,7 +2,7 @@ import { callJsonRpc } from '@/api/request'
export const getUtxosFromViewKey = async (
viewKey: string,
startBlock: number | null = 0,
startBlock: number = 0,
endBlock: number | null = null,
maxSearchDepth: number = 1000
): Promise<any> => {
@ -17,7 +17,7 @@ export const getUtxosFromViewKey = async (
export const getBalance = async (
viewKey: string,
startBlock: number | null = 0,
startBlock: number = 0,
endBlock: number | null = null,
maxSearchDepth: number = 1000
): Promise<any> => {

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import type { ButtonProps } from '@/interface'
import { computed } from 'vue'
import { SpinnerCommon } from '@/components'
const props = withDefaults(defineProps<ButtonProps>(), {
type: 'default',

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ButtonCommon, CardBase, FormCommon } from '@/components'
interface Props {
title?: string

View File

@ -7,7 +7,6 @@ import type {
WalletState,
} from '@/interface'
import initWasm, { generate_seed, address_from_seed, validate_seed_phrase } from '@neptune/wasm'
import { toFiniteNumber } from '@/utils'
let wasmInitialized = false
let initPromise: Promise<void> | null = null
@ -172,7 +171,9 @@ export function useNeptuneWallet() {
store.setKeystorePath(keystoreFile.filePath)
if ('minBlockHeight' in keystoreFile) {
const height = keystoreFile.minBlockHeight
store.setMinBlockHeight(toFiniteNumber(height))
store.setMinBlockHeight(
typeof height === 'number' && Number.isFinite(height) ? height : null
)
}
return true
} catch (err) {
@ -181,69 +182,82 @@ export function useNeptuneWallet() {
}
}
// ===== API METHODS =====
const persistMinBlockHeight = async (utxos: any[]) => {
const keystorePath = store.getKeystorePath
if (!keystorePath) return
try {
const minBlockHeight = utxos.reduce((min, utxo) => {
const h = +(
utxo?.blockHeight ??
utxo?.block_height ??
utxo?.height ??
utxo?.block?.height
)
return Number.isFinite(h) && (min === null || h < min) ? h : min
}, null)
const heights = utxos
.map((utxo) => {
const rawHeight = [utxo?.blockHeight, utxo?.block_height, utxo?.height, utxo?.block?.height]
.find((h) => h !== null && h !== undefined) ?? null
const response = await (window as any).walletApi.updateMinBlockHeight(
keystorePath,
minBlockHeight
)
if (!response.success) throw new Error('Failed to update min block height')
const numericHeight =
typeof rawHeight === 'string' ? Number(rawHeight) : rawHeight
return Number.isFinite(numericHeight) ? Number(numericHeight) : null
})
.filter((height): height is number => height !== null)
const minBlockHeight = heights.length ? Math.min(...heights) : null
await (window as any).walletApi.updateMinBlockHeight(keystorePath, minBlockHeight)
store.setMinBlockHeight(minBlockHeight)
} catch (err) {
console.error('Error saving min block height:', err)
throw err
}
}
const loadMinBlockHeightFromKeystore = async (): Promise<number | null> => {
const keystorePath = store.getKeystorePath
if (!keystorePath) return null
try {
const response = await (window as any).walletApi.getMinBlockHeight(keystorePath)
if (!response?.success) return null
const minBlockHeight = toFiniteNumber(response.minBlockHeight)
if (response?.success) {
const minBlockHeight =
typeof response.minBlockHeight === 'number' && Number.isFinite(response.minBlockHeight)
? response.minBlockHeight
: null
store.setMinBlockHeight(minBlockHeight)
return minBlockHeight
store.setMinBlockHeight(minBlockHeight)
return minBlockHeight
}
} catch (err) {
console.error('Error loading min block height:', err)
throw err
}
return null
}
// ===== API METHODS =====
const getUtxos = async (): Promise<any> => {
try {
if (!store.getViewKey) {
throw new Error('No view key available. Please import or generate a wallet first.')
}
let startBlock: number | null = store.getMinBlockHeight
if (startBlock == null) startBlock = await loadMinBlockHeightFromKeystore()
let startBlock: number | null | undefined = store.getMinBlockHeight
if (startBlock === null || startBlock === undefined) {
startBlock = await loadMinBlockHeightFromKeystore()
}
const response = await API.getUtxosFromViewKey(
store.getViewKey || '',
toFiniteNumber(startBlock, 0)
typeof startBlock === 'number' && Number.isFinite(startBlock) ? startBlock : 0
)
const result = response?.result || response
const utxos = result?.utxos ?? result
const utxoList = Array.isArray(utxos) ? utxos : []
const utxoList = Array.isArray(result?.utxos)
? result.utxos
: Array.isArray(result)
? result
: []
store.setUtxos(utxoList)
@ -265,10 +279,9 @@ export function useNeptuneWallet() {
const response = await API.getBalance(
store.getViewKey || '',
toFiniteNumber(startBlock, 0)
typeof startBlock === 'number' && Number.isFinite(startBlock) ? startBlock : 0
)
const result = response?.result || response
store.setBalance(result?.balance || result)
store.setPendingBalance(result?.pendingBalance || result)
return {
@ -305,7 +318,9 @@ export function useNeptuneWallet() {
}
}
const buildTransaction = async (args: PayloadBuildTransaction): Promise<any> => {
const buildTransaction = async (
args: PayloadBuildTransaction
): Promise<any> => {
let minBlockHeight: number | null | undefined = store.getMinBlockHeight
if (minBlockHeight === null || minBlockHeight === undefined) {
minBlockHeight = await loadMinBlockHeightFromKeystore()
@ -314,7 +329,10 @@ export function useNeptuneWallet() {
const payload = {
spendingKeyHex: store.getSpendingKey,
inputAdditionRecords: args.inputAdditionRecords,
minBlockHeight: toFiniteNumber(minBlockHeight, 0),
minBlockHeight:
typeof minBlockHeight === 'number' && Number.isFinite(minBlockHeight)
? minBlockHeight
: 0,
outputAddresses: args.outputAddresses,
outputAmounts: args.outputAmounts,
fee: args.fee,

View File

@ -1,7 +1,6 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { WalletState } from '@/interface'
import { toFiniteNumber } from '@/utils'
export const useNeptuneStore = defineStore('neptune', () => {
const defaultNetwork = (import.meta.env.VITE_NODE_NETWORK || 'mainnet') as 'mainnet' | 'testnet'
@ -66,7 +65,10 @@ export const useNeptuneStore = defineStore('neptune', () => {
}
const setMinBlockHeight = (minBlockHeight: number | null) => {
wallet.value.minBlockHeight = toFiniteNumber(minBlockHeight)
wallet.value.minBlockHeight =
typeof minBlockHeight === 'number' && Number.isFinite(minBlockHeight)
? minBlockHeight
: null
}
const setWallet = (walletData: Partial<WalletState>) => {

View File

@ -1,3 +0,0 @@
export function toFiniteNumber(value: number | null, defaultValue?: number): number | null {
return Number.isFinite(value) ? value : (defaultValue ?? null)
}

View File

@ -2,4 +2,3 @@ export * from './constants/code'
export * from './constants/constants'
export * from './helpers/format'
export * from './helpers/seedPhrase'
export * from './helpers/helpers'

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue'
import { PasswordForm } from '@/components'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/authStore'

View File

@ -1,4 +1,6 @@
<script setup lang="ts">
import { ButtonCommon } from '@/components'
const emit = defineEmits<{
goToCreate: []
goToRecover: []

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { ButtonCommon } from '@/components'
import { useNeptuneStore } from '@/stores/neptuneStore'
import { message } from 'ant-design-vue'

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ButtonCommon } from '@/components'
import { useNeptuneStore } from '@/stores/neptuneStore'
import { message } from 'ant-design-vue'

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ButtonCommon, FormCommon } from '@/components'
import { useNeptuneStore } from '@/stores'
const emit = defineEmits<{

View File

@ -4,6 +4,7 @@ import { SeedPhraseDisplayComponent, ConfirmSeedComponent } from '..'
import { CreatePasswordStep, WalletCreatedStep } from '.'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { message } from 'ant-design-vue'
import { CardBaseScrollable } from '@/components'
import { useRouter } from 'vue-router'
const emit = defineEmits<{

View File

@ -1,4 +1,5 @@
<script setup lang="ts">
import { ButtonCommon } from '@/components'
import { useNeptuneStore } from '@/stores/neptuneStore'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { message } from 'ant-design-vue'

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ButtonCommon, PasswordForm } from '@/components'
import { RecoverSeedComponent } from '..'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { message } from 'ant-design-vue'

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { TabsCommon, TabPaneCommon } from '@/components'
import { WalletTab, NetworkTab, UTXOTab } from './components'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { formatNumberToLocaleString } from '@/utils'
import { CardBase, SpinnerCommon } from '@/components'
import { useNeptuneStore } from '@/stores/neptuneStore'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { message } from 'ant-design-vue'

View File

@ -1,13 +1,13 @@
<script setup lang="ts">
import { ref, computed, onMounted, inject, watch, type ComputedRef } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { Table, message } from 'ant-design-vue'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { useNeptuneStore } from '@/stores/neptuneStore'
import { CardBaseScrollable, SpinnerCommon } from '@/components'
import { columns } from '../utils'
const { getUtxos } = useNeptuneWallet()
const neptuneStore = useNeptuneStore()
const activeTabKey = inject<ComputedRef<string>>('activeTabKey')
const loading = ref(false)
@ -55,13 +55,6 @@ const loadUtxos = async () => {
onMounted(() => {
loadUtxos()
})
watch(
() => activeTabKey?.value,
(newTab) => {
if (newTab === 'UTXOs') loadUtxos()
}
)
</script>
<template>

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ButtonCommon, ModalCommon } from '@/components'
interface Props {
isLoading?: boolean

View File

@ -27,9 +27,7 @@ const props = defineProps<Props>()
<style lang="scss" scoped>
.balance-section {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
margin-bottom: var(--spacing-xl);
padding-bottom: var(--spacing-lg);
border-bottom: 2px solid var(--border-color);

View File

@ -1,8 +1,15 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, inject, watch, type ComputedRef } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useNeptuneStore } from '@/stores/neptuneStore'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { message } from 'ant-design-vue'
import {
ButtonCommon,
CardBaseScrollable,
ModalCommon,
SpinnerCommon,
PasswordForm,
} from '@/components'
import SeedPhraseDisplayComponent from '@/views/Auth/components/SeedPhraseDisplayComponent.vue'
import SendTransactionComponent from './SendTransactionComponent.vue'
import { WalletAddress, WalletBalance } from '.'
@ -16,7 +23,6 @@ const {
broadcastSignedTransaction,
decryptKeystore,
} = useNeptuneWallet()
const activeTabKey = inject<ComputedRef<string>>('activeTabKey')
const availableBalance = ref<string>('0.00000000')
const pendingBalance = ref<string>('0.00000000')
@ -166,13 +172,6 @@ onMounted(() => {
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
watch(
() => activeTabKey?.value,
(newTab) => {
if (newTab === 'WALLET') loadWalletData()
}
)
</script>
<template>