diff --git a/electron/ipcHandlers.ts b/electron/ipcHandlers.ts index 3f17a12..4ea0842 100644 --- a/electron/ipcHandlers.ts +++ b/electron/ipcHandlers.ts @@ -80,13 +80,95 @@ ipcMain.handle('wallet:checkKeystore', async () => { if (!newestFile) return { exists: false, filePath: null } - return { exists: true, filePath: path.join(walletDir, newestFile) } + const resolvedPath = path.join(walletDir, newestFile) + let minBlockHeight: number | null = null + + try { + const json = fs.readFileSync(resolvedPath, 'utf-8') + const data = JSON.parse(json) + const height = data?.minBlockHeight + + 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, minBlockHeight } } catch (error) { console.error('Error checking keystore ipc:', error) - return { exists: false, filePath: null, error: String(error) } + return { exists: false, filePath: null, minBlockHeight: null, error: String(error) } } }) +ipcMain.handle( + 'wallet:updateMinBlockHeight', + async (_event, filePath: string | null, minBlockHeight: number | null) => { + if (!filePath) { + return { success: false, error: 'No keystore file path provided.' } + } + + try { + const normalizedPath = path.isAbsolute(filePath) + ? filePath + : path.join(process.cwd(), filePath) + + if (!fs.existsSync(normalizedPath)) { + return { success: false, error: 'Keystore file not found.' } + } + + const fileContents = fs.readFileSync(normalizedPath, 'utf-8') + const walletJson = JSON.parse(fileContents) + + if (minBlockHeight === null || Number.isNaN(minBlockHeight)) { + walletJson.minBlockHeight = null + } else { + walletJson.minBlockHeight = minBlockHeight + } + + fs.writeFileSync(normalizedPath, JSON.stringify(walletJson, null, 2), 'utf-8') + + return { success: true, minBlockHeight } + } catch (error) { + console.error('Error updating min block height:', error) + return { success: false, error: String(error) } + } + } +) + +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 } + } + + 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 } + } + } +) + ipcMain.handle('wallet:generateKeysFromSeed', async (_event, seedPhrase: string[]) => { try { const wallet = new neptuneNative.WalletManager() @@ -97,15 +179,19 @@ ipcMain.handle('wallet:generateKeysFromSeed', async (_event, seedPhrase: string[ } }) -ipcMain.handle('wallet:buildTransactionWithPrimitiveProof', async (_event, args) => { +ipcMain.handle('wallet:buildTransaction', async (_event, args) => { const { spendingKeyHex, inputAdditionRecords, outputAddresses, outputAmounts, fee } = args try { const builder = new neptuneNative.SimpleTransactionBuilder() - const result = await builder.buildTransactionWithPrimitiveProof( + const result = await builder.buildTransaction( 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, outputAddresses, outputAmounts, fee diff --git a/electron/preload.ts b/electron/preload.ts index 4b791e0..817977c 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -12,6 +12,9 @@ contextBridge.exposeInMainWorld('walletApi', { checkKeystore: () => ipcRenderer.invoke('wallet:checkKeystore'), generateKeysFromSeed: (seedPhrase: string[]) => ipcRenderer.invoke('wallet:generateKeysFromSeed', seedPhrase), - buildTransactionWithPrimitiveProof: (args: any) => - ipcRenderer.invoke('wallet:buildTransactionWithPrimitiveProof', args), + buildTransaction: (args: any) => ipcRenderer.invoke('wallet:buildTransaction', args), + updateMinBlockHeight: (filePath: string | null, minBlockHeight: number | null) => + ipcRenderer.invoke('wallet:updateMinBlockHeight', filePath, minBlockHeight), + getMinBlockHeight: (filePath: string | null) => + ipcRenderer.invoke('wallet:getMinBlockHeight', filePath), }) diff --git a/packages/neptune-native/index.d.ts b/packages/neptune-native/index.d.ts index a5d663b..c59ff10 100644 --- a/packages/neptune-native/index.d.ts +++ b/packages/neptune-native/index.d.ts @@ -3,12 +3,9 @@ /* auto-generated by NAPI-RS */ -/** Module initialization */ export declare function initNativeModule(): string -/** Quick VM test (can call immediately) */ export declare function quickVmTest(): string export declare function getVersion(): string -/** Wallet manager for key generation and transaction signing */ export declare class WalletManager { constructor() /** @@ -64,7 +61,7 @@ export declare class WalletManager { getStateCall(rpcUrl: string): Promise /** Call mempool_submitTransaction to broadcast a pre-built transaction */ submitTransactionCall(rpcUrl: string, transactionHex: string): Promise - getUtxosFromViewKeyCall(rpcUrl: string, viewKeyHex: string, startBlock: number, endBlock: number, maxSearchDepth?: number | undefined | null): Promise + getUtxosFromViewKeyCall(rpcUrl: string, viewKeyHex: string, startBlock: number, maxSearchDepth?: number | undefined | null): Promise getArchivalMutatorSet(rpcUrl: string): Promise /** * Build JSON-RPC request to find the canonical block that created a UTXO (by addition_record) @@ -78,5 +75,5 @@ export declare class WalletManager { } export declare class SimpleTransactionBuilder { constructor() - buildTransactionWithPrimitiveProof(rpcUrl: string, spendingKeyHex: string, inputAdditionRecords: Array, outputAddresses: Array, outputAmounts: Array, fee: string): Promise + buildTransaction(rpcUrl: string, spendingKeyHex: string, inputAdditionRecords: Array, minBlockHeight: number, outputAddresses: Array, outputAmounts: Array, fee: string): Promise } diff --git a/packages/neptune-native/neptune-native.darwin-arm64.node b/packages/neptune-native/neptune-native.darwin-arm64.node index f561130..46809d2 100755 Binary files a/packages/neptune-native/neptune-native.darwin-arm64.node and b/packages/neptune-native/neptune-native.darwin-arm64.node differ diff --git a/src/composables/useNeptuneWallet.ts b/src/composables/useNeptuneWallet.ts index f2c0fc9..6d4ecae 100644 --- a/src/composables/useNeptuneWallet.ts +++ b/src/composables/useNeptuneWallet.ts @@ -125,6 +125,8 @@ export function useNeptuneWallet() { const addressResult = await getAddressFromSeed(seedPhrase) store.setAddress(addressResult) + + await loadMinBlockHeightFromKeystore() } catch (err) { if ( err instanceof Error && @@ -142,6 +144,7 @@ export function useNeptuneWallet() { try { const result = await (window as any).walletApi.createKeystore(seed, password) store.setKeystorePath(result.filePath) + store.setMinBlockHeight(null) return result.filePath } catch (err) { console.error('Error creating keystore:', err) @@ -166,6 +169,12 @@ export function useNeptuneWallet() { if (!keystoreFile.exists) return false store.setKeystorePath(keystoreFile.filePath) + if ('minBlockHeight' in keystoreFile) { + const height = keystoreFile.minBlockHeight + store.setMinBlockHeight( + typeof height === 'number' && Number.isFinite(height) ? height : null + ) + } return true } catch (err) { console.error('Error checking keystore:', err) @@ -175,16 +184,84 @@ export function useNeptuneWallet() { // ===== API METHODS ===== + const persistMinBlockHeight = async (utxos: any[]) => { + const keystorePath = store.getKeystorePath + + if (!keystorePath) return + + try { + 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 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) + } + } + + const loadMinBlockHeightFromKeystore = async (): Promise => { + const keystorePath = store.getKeystorePath + + if (!keystorePath) return null + + try { + const response = await (window as any).walletApi.getMinBlockHeight(keystorePath) + + if (response?.success) { + const minBlockHeight = + typeof response.minBlockHeight === 'number' && Number.isFinite(response.minBlockHeight) + ? response.minBlockHeight + : null + + store.setMinBlockHeight(minBlockHeight) + return minBlockHeight + } + } catch (err) { + console.error('Error loading min block height:', err) + } + + return null + } + const getUtxos = async (): Promise => { try { if (!store.getViewKey) { throw new Error('No view key available. Please import or generate a wallet first.') } - const response = await API.getUtxosFromViewKey(store.getViewKey || '') + let startBlock: number | null | undefined = store.getMinBlockHeight + + if (startBlock === null || startBlock === undefined) { + startBlock = await loadMinBlockHeightFromKeystore() + } + + const response = await API.getUtxosFromViewKey( + store.getViewKey || '', + typeof startBlock === 'number' && Number.isFinite(startBlock) ? startBlock : 0 + ) const result = response?.result || response - store.setUtxos(result.utxos || result || []) + const utxoList = Array.isArray(result?.utxos) + ? result.utxos + : Array.isArray(result) + ? result + : [] + + store.setUtxos(utxoList) + + await persistMinBlockHeight(utxoList) return result } catch (err) { console.error('Error getting UTXOs:', err) @@ -194,7 +271,16 @@ export function useNeptuneWallet() { const getBalance = async (): Promise => { try { - const response = await API.getBalance(store.getViewKey || '') + let startBlock: number | null | undefined = store.getMinBlockHeight + + if (startBlock === null || startBlock === undefined) { + startBlock = await loadMinBlockHeightFromKeystore() + } + + const response = await API.getBalance( + store.getViewKey || '', + typeof startBlock === 'number' && Number.isFinite(startBlock) ? startBlock : 0 + ) const result = response?.result || response store.setBalance(result?.balance || result) store.setPendingBalance(result?.pendingBalance || result) @@ -232,17 +318,26 @@ export function useNeptuneWallet() { } } - const buildTransactionWithPrimitiveProof = async ( + const buildTransaction = async ( args: PayloadBuildTransaction ): Promise => { + let minBlockHeight: number | null | undefined = store.getMinBlockHeight + if (minBlockHeight === null || minBlockHeight === undefined) { + minBlockHeight = await loadMinBlockHeightFromKeystore() + } + const payload = { spendingKeyHex: store.getSpendingKey, inputAdditionRecords: args.inputAdditionRecords, + minBlockHeight: + typeof minBlockHeight === 'number' && Number.isFinite(minBlockHeight) + ? minBlockHeight + : 0, outputAddresses: args.outputAddresses, outputAmounts: args.outputAmounts, fee: args.fee, } - return await (window as any).walletApi.buildTransactionWithPrimitiveProof(payload) + return await (window as any).walletApi.buildTransaction(payload) } const broadcastSignedTransaction = async (transactionHex: string): Promise => { @@ -290,7 +385,7 @@ export function useNeptuneWallet() { getBalance, getBlockHeight, getNetworkInfo, - buildTransactionWithPrimitiveProof, + buildTransaction, broadcastSignedTransaction, decryptKeystore, createKeystore, diff --git a/src/interface/neptune.ts b/src/interface/neptune.ts index 90be6e5..e8066ce 100644 --- a/src/interface/neptune.ts +++ b/src/interface/neptune.ts @@ -9,6 +9,7 @@ export interface WalletState { balance?: string | null pendingBalance?: string | null utxos?: Utxo[] + minBlockHeight?: number | null } export interface GenerateSeedResult { diff --git a/src/stores/neptuneStore.ts b/src/stores/neptuneStore.ts index c740f2b..4f53876 100644 --- a/src/stores/neptuneStore.ts +++ b/src/stores/neptuneStore.ts @@ -17,6 +17,7 @@ export const useNeptuneStore = defineStore('neptune', () => { balance: null, pendingBalance: null, utxos: [], + minBlockHeight: null, }) const keystorePath = ref(null) @@ -63,6 +64,13 @@ export const useNeptuneStore = defineStore('neptune', () => { wallet.value.utxos = utxos } + const setMinBlockHeight = (minBlockHeight: number | null) => { + wallet.value.minBlockHeight = + typeof minBlockHeight === 'number' && Number.isFinite(minBlockHeight) + ? minBlockHeight + : null + } + const setWallet = (walletData: Partial) => { wallet.value = { ...wallet.value, ...walletData } } @@ -83,6 +91,7 @@ export const useNeptuneStore = defineStore('neptune', () => { balance: null, pendingBalance: null, utxos: [], + minBlockHeight: null, } } @@ -99,6 +108,7 @@ export const useNeptuneStore = defineStore('neptune', () => { const getBalance = computed(() => wallet.value.balance) const getPendingBalance = computed(() => wallet.value.pendingBalance) const getUtxos = computed(() => wallet.value.utxos) + const getMinBlockHeight = computed(() => wallet.value.minBlockHeight ?? null) const hasWallet = computed(() => wallet.value.address !== null) const getKeystorePath = computed(() => keystorePath.value) return { @@ -114,6 +124,7 @@ export const useNeptuneStore = defineStore('neptune', () => { getBalance, getPendingBalance, getUtxos, + getMinBlockHeight, hasWallet, getKeystorePath, setSeedPhrase, @@ -126,6 +137,7 @@ export const useNeptuneStore = defineStore('neptune', () => { setBalance, setPendingBalance, setUtxos, + setMinBlockHeight, setWallet, setKeystorePath, clearWallet, diff --git a/src/views/Home/components/wallet-tab/WalletInfo.vue b/src/views/Home/components/wallet-tab/WalletInfo.vue index 208ad17..60a3497 100644 --- a/src/views/Home/components/wallet-tab/WalletInfo.vue +++ b/src/views/Home/components/wallet-tab/WalletInfo.vue @@ -19,7 +19,7 @@ const neptuneStore = useNeptuneStore() const { getBalance, saveKeystoreAs, - buildTransactionWithPrimitiveProof, + buildTransaction, broadcastSignedTransaction, decryptKeystore, } = useNeptuneWallet() @@ -78,7 +78,7 @@ const handleSendTransaction = async (data: { fee: data.fee, } - const result = await buildTransactionWithPrimitiveProof(payload) + const result = await buildTransaction(payload) if (!result.success) { message.error('Failed to build transaction') return