2025-11-05 04:14:37 +07:00
|
|
|
import { ipcMain, dialog, app } from 'electron'
|
2025-10-31 21:56:47 +07:00
|
|
|
import fs from 'fs'
|
|
|
|
|
import path from 'path'
|
2025-11-05 04:14:37 +07:00
|
|
|
import { encrypt, fromEncryptedJson } from './utils/keystore'
|
2025-11-11 17:16:43 +07:00
|
|
|
import logger from './logger'
|
2025-10-31 01:22:35 +07:00
|
|
|
|
2025-11-14 15:23:46 +07:00
|
|
|
let neptuneNative: any = null
|
|
|
|
|
|
|
|
|
|
function loadNativeModule() {
|
|
|
|
|
try {
|
|
|
|
|
// Try normal require first (works in dev)
|
|
|
|
|
neptuneNative = require('@neptune/native')
|
|
|
|
|
logger.info('[Native] Loaded native module')
|
|
|
|
|
return
|
|
|
|
|
} catch (err) {
|
|
|
|
|
logger.error('[Native] Failed to load native module')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback: load from Resources in packaged app
|
|
|
|
|
try {
|
|
|
|
|
const platform = process.platform
|
|
|
|
|
const arch = process.arch
|
|
|
|
|
|
|
|
|
|
// Determine the correct .node file
|
|
|
|
|
let nodeFileName = ''
|
|
|
|
|
if (platform === 'win32' && arch === 'x64') {
|
|
|
|
|
nodeFileName = 'neptune-native.win32-x64-msvc.node'
|
|
|
|
|
} else if (platform === 'linux' && arch === 'x64') {
|
|
|
|
|
nodeFileName = 'neptune-native.linux-x64-gnu.node'
|
|
|
|
|
} else if (platform === 'darwin' && arch === 'arm64') {
|
|
|
|
|
nodeFileName = 'neptune-native.darwin-arm64.node'
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error(`Unsupported platform: ${platform}-${arch}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try multiple possible locations
|
|
|
|
|
const possiblePaths = [
|
|
|
|
|
path.join(process.resourcesPath, 'packages', 'neptune-native', nodeFileName),
|
|
|
|
|
path.join(process.resourcesPath, 'neptune-native', nodeFileName),
|
|
|
|
|
path.join(process.resourcesPath, nodeFileName),
|
|
|
|
|
path.join(process.resourcesPath, 'app.asar.unpacked', 'packages', 'neptune-native', nodeFileName),
|
|
|
|
|
path.join(process.resourcesPath, 'app.asar.unpacked', 'node_modules', '@neptune', 'native', nodeFileName),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
let nativeBinding: any = null
|
|
|
|
|
for (const nodePath of possiblePaths) {
|
|
|
|
|
if (fs.existsSync(nodePath)) {
|
|
|
|
|
nativeBinding = require(nodePath)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!nativeBinding) {
|
|
|
|
|
throw new Error(`Native module not found`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
neptuneNative = nativeBinding
|
|
|
|
|
logger.info('[Native] Successfully loaded native module')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('[Native] Failed to load native module:')
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize the native module
|
|
|
|
|
loadNativeModule()
|
2025-11-07 18:27:37 +07:00
|
|
|
|
2025-11-14 00:55:55 +07:00
|
|
|
const WALLETS_DIR = path.resolve(app.getPath('userData'), 'wallets')
|
|
|
|
|
fs.mkdirSync(WALLETS_DIR, { recursive: true })
|
|
|
|
|
|
|
|
|
|
function assertBasename(name: string) {
|
|
|
|
|
if (!name || name !== path.basename(name)) {
|
|
|
|
|
throw new Error('Invalid file name')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function safeResolvePath(fileName: string) {
|
|
|
|
|
assertBasename(fileName)
|
|
|
|
|
const candidate = path.join(WALLETS_DIR, fileName)
|
|
|
|
|
const realBase = fs.realpathSync(WALLETS_DIR)
|
|
|
|
|
const realCandidate = fs.existsSync(candidate) ? fs.realpathSync(candidate) : candidate
|
|
|
|
|
if (!(realCandidate === realBase || realCandidate.startsWith(realBase + path.sep))) {
|
|
|
|
|
throw new Error('Access denied')
|
|
|
|
|
}
|
|
|
|
|
return realCandidate
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-31 01:22:35 +07:00
|
|
|
ipcMain.handle('wallet:createKeystore', async (_event, seed, password) => {
|
2025-10-31 21:56:47 +07:00
|
|
|
try {
|
2025-11-05 04:14:37 +07:00
|
|
|
const keystore = await encrypt(seed, password)
|
|
|
|
|
const timestamp = Date.now()
|
|
|
|
|
const fileName = `neptune-wallet-${timestamp}.json`
|
2025-11-14 00:55:55 +07:00
|
|
|
const filePath = path.join(WALLETS_DIR, fileName)
|
|
|
|
|
fs.writeFileSync(filePath, keystore, 'utf-8')
|
|
|
|
|
return { success: true, fileName }
|
2025-11-11 17:16:43 +07:00
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error('Error creating keystore:', error.message)
|
2025-11-05 04:14:37 +07:00
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2025-11-14 00:55:55 +07:00
|
|
|
ipcMain.handle('wallet:saveKeystoreAs', async (_event, fileName: string) => {
|
2025-11-05 04:14:37 +07:00
|
|
|
try {
|
2025-11-14 00:55:55 +07:00
|
|
|
const pathToFileName = safeResolvePath(fileName)
|
|
|
|
|
const keystore = fs.readFileSync(pathToFileName, 'utf-8')
|
2025-11-05 04:14:37 +07:00
|
|
|
const timestamp = Date.now()
|
|
|
|
|
const defaultName = `neptune-wallet-${timestamp}.json`
|
|
|
|
|
const { canceled, filePath } = await dialog.showSaveDialog({
|
|
|
|
|
title: 'Save Keystore File',
|
|
|
|
|
defaultPath: path.join(app.getPath('documents'), defaultName),
|
|
|
|
|
filters: [{ name: 'JSON', extensions: ['json'] }],
|
|
|
|
|
})
|
|
|
|
|
|
2025-11-14 00:55:55 +07:00
|
|
|
if (canceled || !filePath) return { success: false, filePath: null }
|
|
|
|
|
if (path.extname(filePath).toLowerCase() !== '.json') {
|
|
|
|
|
throw new Error('Invalid file extension. Please save as a .json file.')
|
|
|
|
|
}
|
2025-11-05 04:14:37 +07:00
|
|
|
|
|
|
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
2025-11-14 00:55:55 +07:00
|
|
|
fs.writeFileSync(filePath, keystore, 'utf-8')
|
|
|
|
|
return { success: true, filePath }
|
2025-11-11 17:16:43 +07:00
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error('Error saving keystore (Save As):', error.message)
|
2025-11-05 04:14:37 +07:00
|
|
|
throw error
|
2025-10-31 21:56:47 +07:00
|
|
|
}
|
|
|
|
|
})
|
2025-10-31 01:22:35 +07:00
|
|
|
|
2025-11-14 00:55:55 +07:00
|
|
|
ipcMain.handle('wallet:decryptKeystore', async (_event, fileName: string, password: string) => {
|
2025-10-31 21:56:47 +07:00
|
|
|
try {
|
2025-11-14 00:55:55 +07:00
|
|
|
const filePath = safeResolvePath(fileName)
|
2025-10-31 21:56:47 +07:00
|
|
|
const json = fs.readFileSync(filePath, 'utf-8')
|
2025-11-05 04:14:37 +07:00
|
|
|
const phrase = await fromEncryptedJson(json, password)
|
|
|
|
|
return { phrase }
|
2025-11-11 17:16:43 +07:00
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error('Error decrypting keystore ipc:', error.message)
|
2025-11-05 04:14:37 +07:00
|
|
|
throw error
|
2025-10-31 21:56:47 +07:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
ipcMain.handle('wallet:checkKeystore', async () => {
|
|
|
|
|
try {
|
2025-11-14 00:55:55 +07:00
|
|
|
if (!fs.existsSync(WALLETS_DIR)) return { exists: false, fileName: null }
|
2025-11-07 18:27:37 +07:00
|
|
|
const newestFile = fs
|
2025-11-14 00:55:55 +07:00
|
|
|
.readdirSync(WALLETS_DIR)
|
2025-11-07 18:27:37 +07:00
|
|
|
.filter((f) => f.endsWith('.json'))
|
|
|
|
|
.sort(
|
|
|
|
|
(a, b) =>
|
2025-11-14 00:55:55 +07:00
|
|
|
fs.statSync(path.join(WALLETS_DIR, b)).mtime.getTime() -
|
|
|
|
|
fs.statSync(path.join(WALLETS_DIR, a)).mtime.getTime()
|
2025-11-07 18:27:37 +07:00
|
|
|
)[0]
|
|
|
|
|
|
2025-11-14 00:55:55 +07:00
|
|
|
if (!newestFile) return { exists: false, fileName: null }
|
|
|
|
|
const resolvedPath = safeResolvePath(newestFile)
|
2025-11-10 15:50:35 +07:00
|
|
|
let minBlockHeight: number | null = null
|
|
|
|
|
try {
|
|
|
|
|
const json = fs.readFileSync(resolvedPath, 'utf-8')
|
|
|
|
|
const data = JSON.parse(json)
|
|
|
|
|
const height = data?.minBlockHeight
|
2025-11-14 00:55:55 +07:00
|
|
|
if (Number.isFinite(height)) minBlockHeight = height
|
2025-11-11 17:16:43 +07:00
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.warn('Unable to read minBlockHeight from keystore:', error.message)
|
2025-11-10 15:50:35 +07:00
|
|
|
}
|
2025-11-14 00:55:55 +07:00
|
|
|
return { exists: true, fileName: newestFile, minBlockHeight }
|
2025-11-11 17:16:43 +07:00
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error('Error checking keystore ipc:', error.message)
|
2025-11-14 00:55:55 +07:00
|
|
|
return { exists: false, fileName: null, minBlockHeight: null, error: error.message }
|
2025-10-31 21:56:47 +07:00
|
|
|
}
|
|
|
|
|
})
|
2025-11-07 18:27:37 +07:00
|
|
|
|
2025-11-10 15:50:35 +07:00
|
|
|
ipcMain.handle(
|
|
|
|
|
'wallet:updateMinBlockHeight',
|
2025-11-14 00:55:55 +07:00
|
|
|
async (_event, fileName: string | null, minBlockHeight: number | null) => {
|
|
|
|
|
if (!fileName) {
|
|
|
|
|
return { success: false, error: 'No keystore file name provided.' }
|
2025-11-10 15:50:35 +07:00
|
|
|
}
|
|
|
|
|
try {
|
2025-11-14 00:55:55 +07:00
|
|
|
const filePath = safeResolvePath(fileName)
|
|
|
|
|
if (!fs.existsSync(filePath)) {
|
2025-11-10 15:50:35 +07:00
|
|
|
return { success: false, error: 'Keystore file not found.' }
|
|
|
|
|
}
|
2025-11-14 00:55:55 +07:00
|
|
|
const fileContents = fs.readFileSync(filePath, 'utf-8')
|
2025-11-10 15:50:35 +07:00
|
|
|
const walletJson = JSON.parse(fileContents)
|
2025-11-14 00:55:55 +07:00
|
|
|
walletJson.minBlockHeight = Number.isFinite(minBlockHeight) ? minBlockHeight : null
|
|
|
|
|
fs.writeFileSync(filePath, JSON.stringify(walletJson, null, 2), 'utf-8')
|
2025-11-10 15:50:35 +07:00
|
|
|
return { success: true, minBlockHeight }
|
2025-11-11 17:16:43 +07:00
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error('Error updating min block height:', error.message)
|
|
|
|
|
return { success: false, error: error.message }
|
2025-11-10 15:50:35 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
2025-11-14 00:55:55 +07:00
|
|
|
ipcMain.handle('wallet:getMinBlockHeight', async (_event, fileName: string | null) => {
|
|
|
|
|
if (!fileName) {
|
|
|
|
|
return { success: false, error: 'No keystore file name provided.', minBlockHeight: null }
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const filePath = safeResolvePath(fileName)
|
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
|
|
|
return { success: false, error: 'Keystore file not found.', minBlockHeight: null }
|
2025-11-10 15:50:35 +07:00
|
|
|
}
|
2025-11-14 00:55:55 +07:00
|
|
|
const fileContents = fs.readFileSync(filePath, 'utf-8')
|
|
|
|
|
const walletJson = JSON.parse(fileContents)
|
|
|
|
|
const height = walletJson?.minBlockHeight
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
minBlockHeight: Number.isFinite(height) ? height : null,
|
2025-11-10 15:50:35 +07:00
|
|
|
}
|
2025-11-14 00:55:55 +07:00
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error('Error reading min block height:', error.message)
|
|
|
|
|
return { success: false, error: error.message, minBlockHeight: null }
|
2025-11-10 15:50:35 +07:00
|
|
|
}
|
2025-11-14 00:55:55 +07:00
|
|
|
})
|
2025-11-10 15:50:35 +07:00
|
|
|
|
2025-11-07 18:27:37 +07:00
|
|
|
ipcMain.handle('wallet:generateKeysFromSeed', async (_event, seedPhrase: string[]) => {
|
|
|
|
|
try {
|
|
|
|
|
const wallet = new neptuneNative.WalletManager()
|
2025-11-14 00:55:55 +07:00
|
|
|
return JSON.parse(wallet.generateKeysFromSeed(seedPhrase))
|
2025-11-11 17:16:43 +07:00
|
|
|
} catch (error: any) {
|
|
|
|
|
logger.error('Error generating keys from seed ipc:', error.message)
|
2025-11-07 18:27:37 +07:00
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2025-11-10 15:50:35 +07:00
|
|
|
ipcMain.handle('wallet:buildTransaction', async (_event, args) => {
|
2025-11-07 18:27:37 +07:00
|
|
|
const { spendingKeyHex, inputAdditionRecords, outputAddresses, outputAmounts, fee } = args
|
|
|
|
|
try {
|
2025-11-14 00:55:55 +07:00
|
|
|
if (typeof spendingKeyHex !== 'string' || !/^(0x)?[0-9a-fA-F]+$/.test(spendingKeyHex)) {
|
|
|
|
|
throw new Error('Invalid spending key')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!Array.isArray(inputAdditionRecords) ||
|
|
|
|
|
!inputAdditionRecords.every((r) => typeof r === 'string')
|
|
|
|
|
) {
|
|
|
|
|
throw new Error('Invalid inputAdditionRecords')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!Array.isArray(outputAddresses) ||
|
|
|
|
|
!outputAddresses.every((a) => typeof a === 'string')
|
|
|
|
|
) {
|
|
|
|
|
throw new Error('Invalid outputAddresses')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!Array.isArray(outputAmounts) ||
|
|
|
|
|
!outputAmounts.every((a) => typeof a === 'string' && /^\d+(\.\d+)?$/.test(a))
|
|
|
|
|
) {
|
|
|
|
|
throw new Error('Invalid outputAmounts')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof fee !== 'string' || !/^\d+(\.\d+)?$/.test(fee)) {
|
|
|
|
|
throw new Error('Invalid fee')
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-07 18:27:37 +07:00
|
|
|
const builder = new neptuneNative.SimpleTransactionBuilder()
|
2025-11-10 15:50:35 +07:00
|
|
|
const result = await builder.buildTransaction(
|
2025-11-07 18:27:37 +07:00
|
|
|
import.meta.env.VITE_APP_API,
|
|
|
|
|
spendingKeyHex,
|
|
|
|
|
inputAdditionRecords,
|
2025-11-10 15:50:35 +07:00
|
|
|
typeof args?.minBlockHeight === 'number' && Number.isFinite(args.minBlockHeight)
|
|
|
|
|
? args.minBlockHeight
|
|
|
|
|
: 0,
|
2025-11-07 18:27:37 +07:00
|
|
|
outputAddresses,
|
|
|
|
|
outputAmounts,
|
|
|
|
|
fee
|
|
|
|
|
)
|
|
|
|
|
return JSON.parse(result)
|
2025-11-11 17:16:43 +07:00
|
|
|
} catch (error: any) {
|
2025-11-14 00:55:55 +07:00
|
|
|
logger.error('Error building transaction ipc:', error.message)
|
2025-11-07 18:27:37 +07:00
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
})
|
2025-11-11 17:16:43 +07:00
|
|
|
|
2025-11-14 00:55:55 +07:00
|
|
|
ipcMain.on('log:info', (_, ...msg: string[]) => logger.info(...msg))
|
|
|
|
|
ipcMain.on('log:warn', (_, ...msg: string[]) => logger.warn(...msg))
|
|
|
|
|
ipcMain.on('log:error', (_, ...msg: string[]) => logger.error(...msg))
|