- Resolve Android storage dir at runtime (Documents/Data) and reuse it for saves - List/read wallets across candidate dirs with dedupe by latest mtime - Validate wallet filenames to prevent invalid paths - Update UI hints for Desktop vs Android storage locations
246 lines
7.8 KiB
JavaScript
246 lines
7.8 KiB
JavaScript
/**
|
|
* Platform API — single import for all components.
|
|
*
|
|
* On Electron: delegates to window.electronAPI (IPC, preload.cjs).
|
|
* On Android (Capacitor) or browser: uses crypto-service.js + Capacitor Filesystem.
|
|
*/
|
|
|
|
import {
|
|
generateHdWallet, encryptHdWallet, decryptHdWallet,
|
|
generateP2pk, generateP2pkh, generateP2sh, generateP2wpkh, generateP2tr,
|
|
encryptSingleWallet, decryptSingleWallet,
|
|
} from './crypto-service.js'
|
|
|
|
// Electron exposes window.electronAPI via preload.cjs before React loads.
|
|
const isElectron = typeof window !== 'undefined' && window.electronAPI != null
|
|
|
|
// ── Android storage (Capacitor Filesystem) ─────────────────────────────────
|
|
|
|
const WALLET_DIRS = {
|
|
hd: 'wallet-generator/hdwallet',
|
|
single: 'wallet-generator/single',
|
|
}
|
|
|
|
let _fs = null
|
|
let _storageDirectory = null
|
|
async function fs() {
|
|
if (!_fs) {
|
|
const { Filesystem, Directory } = await import('@capacitor/filesystem')
|
|
_fs = { Filesystem, Directory }
|
|
}
|
|
return _fs
|
|
}
|
|
|
|
function walletDir(kind) {
|
|
const dir = WALLET_DIRS[kind]
|
|
if (!dir) throw new Error(`Unsupported wallet kind: '${kind}'.`)
|
|
return dir
|
|
}
|
|
|
|
function normalizeFilename(filename) {
|
|
if (typeof filename !== 'string') throw new Error('Filename must be a string.')
|
|
const trimmed = filename.trim()
|
|
if (!trimmed) throw new Error('Filename is required.')
|
|
if (trimmed.includes('/') || trimmed.includes('\\') || trimmed === '.' || trimmed === '..') {
|
|
throw new Error('Invalid filename.')
|
|
}
|
|
return trimmed
|
|
}
|
|
|
|
function unique(arr) {
|
|
return [...new Set(arr.filter(Boolean))]
|
|
}
|
|
|
|
function toMtime(value) {
|
|
if (!value) return 0
|
|
if (typeof value === 'number') return value
|
|
const parsed = Date.parse(value)
|
|
return Number.isNaN(parsed) ? 0 : parsed
|
|
}
|
|
|
|
function isMissingFileError(err) {
|
|
const msg = String(err?.message || err || '').toLowerCase()
|
|
return (
|
|
msg.includes('not found') ||
|
|
msg.includes('does not exist') ||
|
|
msg.includes('no such file') ||
|
|
msg.includes('directory at')
|
|
)
|
|
}
|
|
|
|
function directoryLabel(directory, Directory) {
|
|
if (directory === Directory.Documents) return 'Documents'
|
|
if (directory === Directory.Data) return 'Data'
|
|
return 'Storage'
|
|
}
|
|
|
|
async function directoryCandidates() {
|
|
const { Directory } = await fs()
|
|
return unique([Directory.Documents, Directory.Data])
|
|
}
|
|
|
|
async function resolveWritableDirectory() {
|
|
if (_storageDirectory) return _storageDirectory
|
|
const { Filesystem } = await fs()
|
|
let lastErr = null
|
|
for (const directory of await directoryCandidates()) {
|
|
try {
|
|
await Filesystem.mkdir({ path: 'wallet-generator', directory, recursive: true })
|
|
_storageDirectory = directory
|
|
return directory
|
|
} catch (err) {
|
|
lastErr = err
|
|
}
|
|
}
|
|
throw lastErr || new Error('No writable storage directory available.')
|
|
}
|
|
|
|
function orderedDirectories(preferred, all) {
|
|
if (!preferred) return all
|
|
return [preferred, ...all.filter(d => d !== preferred)]
|
|
}
|
|
|
|
async function androidSaveWallet(filename, data, kind) {
|
|
const { Filesystem, Directory } = await fs()
|
|
const safeName = normalizeFilename(filename)
|
|
const dirPath = walletDir(kind)
|
|
const path = `${dirPath}/${safeName}`
|
|
const directory = await resolveWritableDirectory()
|
|
await Filesystem.mkdir({ path: dirPath, directory, recursive: true })
|
|
await Filesystem.writeFile({
|
|
path,
|
|
data: JSON.stringify(data, null, 2),
|
|
directory,
|
|
encoding: 'utf8',
|
|
})
|
|
return `${directoryLabel(directory, Directory)}/${path}`
|
|
}
|
|
|
|
async function androidListWallets() {
|
|
const { Filesystem } = await fs()
|
|
const out = { hd: [], single: [] }
|
|
const preferred = await resolveWritableDirectory().catch(() => null)
|
|
const dirs = orderedDirectories(preferred, await directoryCandidates())
|
|
for (const [kind, dir] of Object.entries(WALLET_DIRS)) {
|
|
const byName = new Map()
|
|
try {
|
|
for (const directory of dirs) {
|
|
try {
|
|
await Filesystem.mkdir({ path: dir, directory, recursive: true })
|
|
const { files = [] } = await Filesystem.readdir({ path: dir, directory })
|
|
files
|
|
.filter(f => (f.type === 'file' || !f.type) && f.name?.toLowerCase?.().endsWith('.json'))
|
|
.forEach(f => {
|
|
const item = {
|
|
name: f.name,
|
|
size: f.size || 0,
|
|
mtimeMs: toMtime(f.mtime),
|
|
}
|
|
const prev = byName.get(item.name)
|
|
if (!prev || item.mtimeMs > prev.mtimeMs) byName.set(item.name, item)
|
|
})
|
|
} catch {
|
|
// Try next storage directory.
|
|
}
|
|
}
|
|
} finally {
|
|
out[kind] = Array.from(byName.values()).sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
async function androidReadWallet(kind, filename) {
|
|
const { Filesystem } = await fs()
|
|
const path = `${walletDir(kind)}/${normalizeFilename(filename)}`
|
|
const preferred = await resolveWritableDirectory().catch(() => null)
|
|
const dirs = orderedDirectories(preferred, await directoryCandidates())
|
|
let lastErr = null
|
|
for (const directory of dirs) {
|
|
try {
|
|
const { data } = await Filesystem.readFile({ path, directory, encoding: 'utf8' })
|
|
return JSON.parse(data)
|
|
} catch (err) {
|
|
lastErr = err
|
|
if (!isMissingFileError(err)) throw err
|
|
}
|
|
}
|
|
throw lastErr || new Error('Wallet file not found.')
|
|
}
|
|
|
|
// ── Exported API ─────────────────────────────────────────────────────────────
|
|
|
|
export const api = {
|
|
hdGenerate: async (args) => {
|
|
if (isElectron) return window.electronAPI.hdGenerate(args)
|
|
return generateHdWallet(args)
|
|
},
|
|
|
|
hdEncrypt: async ({ wallet, password }) => {
|
|
if (isElectron) return window.electronAPI.hdEncrypt({ wallet, password })
|
|
return encryptHdWallet(wallet, password)
|
|
},
|
|
|
|
hdDecrypt: async ({ wallet, password }) => {
|
|
if (isElectron) return window.electronAPI.hdDecrypt({ wallet, password })
|
|
return decryptHdWallet(wallet, password)
|
|
},
|
|
|
|
p2pk: async (args) => {
|
|
if (isElectron) return window.electronAPI.p2pk(args)
|
|
return generateP2pk(args.network, args.compressed)
|
|
},
|
|
|
|
p2pkh: async (args) => {
|
|
if (isElectron) return window.electronAPI.p2pkh(args)
|
|
return generateP2pkh(args.network, args.compressed)
|
|
},
|
|
|
|
p2sh: async (args) => {
|
|
if (isElectron) return window.electronAPI.p2sh(args)
|
|
return generateP2sh(args.network, args.m, args.n, args.compressed)
|
|
},
|
|
|
|
p2wpkh: async (args) => {
|
|
if (isElectron) return window.electronAPI.p2wpkh(args)
|
|
return generateP2wpkh(args.network, args.compressed)
|
|
},
|
|
|
|
p2tr: async (args) => {
|
|
if (isElectron) return window.electronAPI.p2tr(args)
|
|
return generateP2tr(args.network)
|
|
},
|
|
|
|
singleEncrypt: async ({ wallet, password }) => {
|
|
if (isElectron) return window.electronAPI.singleEncrypt({ wallet, password })
|
|
return encryptSingleWallet(wallet, password)
|
|
},
|
|
|
|
singleDecrypt: async ({ wallet, password }) => {
|
|
if (isElectron) return window.electronAPI.singleDecrypt({ wallet, password })
|
|
return decryptSingleWallet(wallet, password)
|
|
},
|
|
|
|
saveWallet: async (filename, data, kind) => {
|
|
if (isElectron) return window.electronAPI.saveWallet(filename, data, kind)
|
|
return androidSaveWallet(filename, data, kind)
|
|
},
|
|
|
|
listWallets: async () => {
|
|
if (isElectron) return window.electronAPI.listWallets()
|
|
return androidListWallets()
|
|
},
|
|
|
|
readWallet: async ({ kind, filename }) => {
|
|
if (isElectron) return window.electronAPI.readWallet({ kind, filename })
|
|
return androidReadWallet(kind, filename)
|
|
},
|
|
|
|
getWalletDir: async () => {
|
|
if (isElectron) return window.electronAPI.getWalletDir()
|
|
const { Directory } = await fs()
|
|
const directory = await resolveWritableDirectory()
|
|
return `${directoryLabel(directory, Directory)}/wallet-generator`
|
|
},
|
|
}
|