Compare commits
2 Commits
e785179ee4
...
079b650d60
| Author | SHA1 | Date | |
|---|---|---|---|
| 079b650d60 | |||
| 7b2ff5445b |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,6 +15,7 @@ __pycache__/
|
|||||||
!package-lock.json
|
!package-lock.json
|
||||||
!frontend/package.json
|
!frontend/package.json
|
||||||
!frontend/package-lock.json
|
!frontend/package-lock.json
|
||||||
|
!frontend/capacitor.config.json
|
||||||
|
|
||||||
# Frontend build output and dependencies
|
# Frontend build output and dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
FROM node:22.14.0-bookworm
|
FROM node:22.14.0-bookworm
|
||||||
|
|
||||||
# ── System deps: JDK 17 + tooling ─────────────────────────────────────────────
|
# ── System deps + JDK 21 (required by current Capacitor Android toolchain) ───
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
default-jdk \
|
ca-certificates \
|
||||||
wget unzip \
|
wget unzip \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV JAVA_HOME=/opt/java/openjdk
|
||||||
|
RUN mkdir -p "${JAVA_HOME}" && \
|
||||||
|
wget -qO- "https://api.adoptium.net/v3/binary/latest/21/ga/linux/x64/jdk/hotspot/normal/eclipse?project=jdk" \
|
||||||
|
| tar -xz -C "${JAVA_HOME}" --strip-components=1
|
||||||
|
|
||||||
# ── Android SDK command-line tools ────────────────────────────────────────────
|
# ── Android SDK command-line tools ────────────────────────────────────────────
|
||||||
# https://developer.android.com/studio#command-line-tools-only
|
# https://developer.android.com/studio#command-line-tools-only
|
||||||
ENV ANDROID_SDK_ROOT=/opt/android-sdk
|
ENV ANDROID_SDK_ROOT=/opt/android-sdk
|
||||||
ENV PATH="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${ANDROID_SDK_ROOT}/platform-tools:${PATH}"
|
ENV PATH="${JAVA_HOME}/bin:${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${ANDROID_SDK_ROOT}/platform-tools:${PATH}"
|
||||||
|
|
||||||
RUN mkdir -p "${ANDROID_SDK_ROOT}/cmdline-tools" && \
|
RUN mkdir -p "${ANDROID_SDK_ROOT}/cmdline-tools" && \
|
||||||
wget -q \
|
wget -q \
|
||||||
@@ -51,5 +56,4 @@ RUN cd frontend/android && \
|
|||||||
./gradlew assembleDebug --no-daemon
|
./gradlew assembleDebug --no-daemon
|
||||||
|
|
||||||
# ── Export APK ────────────────────────────────────────────────────────────────
|
# ── Export APK ────────────────────────────────────────────────────────────────
|
||||||
CMD cp frontend/android/app/build/outputs/apk/debug/app-debug.apk \
|
CMD ["cp", "frontend/android/app/build/outputs/apk/debug/app-debug.apk", "/out/wallet-gen-debug.apk"]
|
||||||
/out/wallet-gen-debug.apk
|
|
||||||
|
|||||||
6
frontend/capacitor.config.json
Normal file
6
frontend/capacitor.config.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"appId": "com.walletgen.app",
|
||||||
|
"appName": "wallet-gen",
|
||||||
|
"webDir": "dist",
|
||||||
|
"bundledWebRuntime": false
|
||||||
|
}
|
||||||
990
frontend/package-lock.json
generated
990
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -230,7 +230,7 @@ export default function HDWallet() {
|
|||||||
value={saveFilename}
|
value={saveFilename}
|
||||||
onChange={e => setSaveFilename(e.target.value)}
|
onChange={e => setSaveFilename(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<span className="form-hint">.json · saved to ~/.wallet-generator/hdwallet/</span>
|
<span className="form-hint">.json · Desktop: ~/.wallet-generator/hdwallet/ · Android: app storage</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Encryption password (optional)</label>
|
<label className="form-label">Encryption password (optional)</label>
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ export default function SingleAddress({ initialTab = 'p2pkh' }) {
|
|||||||
value={filename}
|
value={filename}
|
||||||
onChange={e => setFilename(e.target.value)}
|
onChange={e => setFilename(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<span className="form-hint">.json · saved to ~/.wallet-generator/single/</span>
|
<span className="form-hint">.json · Desktop: ~/.wallet-generator/single/ · Android: app storage</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Encryption password (optional)</label>
|
<label className="form-label">Encryption password (optional)</label>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const WALLET_DIRS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _fs = null
|
let _fs = null
|
||||||
|
let _storageDirectory = null
|
||||||
async function fs() {
|
async function fs() {
|
||||||
if (!_fs) {
|
if (!_fs) {
|
||||||
const { Filesystem, Directory } = await import('@capacitor/filesystem')
|
const { Filesystem, Directory } = await import('@capacitor/filesystem')
|
||||||
@@ -30,49 +31,141 @@ async function fs() {
|
|||||||
return _fs
|
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) {
|
async function androidSaveWallet(filename, data, kind) {
|
||||||
const { Filesystem, Directory } = await fs()
|
const { Filesystem, Directory } = await fs()
|
||||||
const path = `${WALLET_DIRS[kind]}/${filename}`
|
const safeName = normalizeFilename(filename)
|
||||||
await Filesystem.mkdir({ path: WALLET_DIRS[kind], directory: Directory.Documents, recursive: true })
|
const dirPath = walletDir(kind)
|
||||||
|
const path = `${dirPath}/${safeName}`
|
||||||
|
const directory = await resolveWritableDirectory()
|
||||||
|
await Filesystem.mkdir({ path: dirPath, directory, recursive: true })
|
||||||
await Filesystem.writeFile({
|
await Filesystem.writeFile({
|
||||||
path,
|
path,
|
||||||
data: JSON.stringify(data, null, 2),
|
data: JSON.stringify(data, null, 2),
|
||||||
directory: Directory.Documents,
|
directory,
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
})
|
})
|
||||||
return path
|
return `${directoryLabel(directory, Directory)}/${path}`
|
||||||
}
|
}
|
||||||
|
|
||||||
async function androidListWallets() {
|
async function androidListWallets() {
|
||||||
const { Filesystem, Directory } = await fs()
|
const { Filesystem } = await fs()
|
||||||
const out = { hd: [], single: [] }
|
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)) {
|
for (const [kind, dir] of Object.entries(WALLET_DIRS)) {
|
||||||
|
const byName = new Map()
|
||||||
try {
|
try {
|
||||||
await Filesystem.mkdir({ path: dir, directory: Directory.Documents, recursive: true })
|
for (const directory of dirs) {
|
||||||
const { files } = await Filesystem.readdir({ path: dir, directory: Directory.Documents })
|
try {
|
||||||
out[kind] = files
|
await Filesystem.mkdir({ path: dir, directory, recursive: true })
|
||||||
.filter(f => (f.type === 'file' || !f.type) && f.name.toLowerCase().endsWith('.json'))
|
const { files = [] } = await Filesystem.readdir({ path: dir, directory })
|
||||||
.map(f => ({
|
files
|
||||||
|
.filter(f => (f.type === 'file' || !f.type) && f.name?.toLowerCase?.().endsWith('.json'))
|
||||||
|
.forEach(f => {
|
||||||
|
const item = {
|
||||||
name: f.name,
|
name: f.name,
|
||||||
size: f.size || 0,
|
size: f.size || 0,
|
||||||
mtimeMs: f.mtime ? (typeof f.mtime === 'number' ? f.mtime : Date.parse(f.mtime)) : 0,
|
mtimeMs: toMtime(f.mtime),
|
||||||
}))
|
}
|
||||||
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
const prev = byName.get(item.name)
|
||||||
|
if (!prev || item.mtimeMs > prev.mtimeMs) byName.set(item.name, item)
|
||||||
|
})
|
||||||
} catch {
|
} catch {
|
||||||
out[kind] = []
|
// Try next storage directory.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
out[kind] = Array.from(byName.values()).sort((a, b) => b.mtimeMs - a.mtimeMs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
async function androidReadWallet(kind, filename) {
|
async function androidReadWallet(kind, filename) {
|
||||||
const { Filesystem, Directory } = await fs()
|
const { Filesystem } = await fs()
|
||||||
const { data } = await Filesystem.readFile({
|
const path = `${walletDir(kind)}/${normalizeFilename(filename)}`
|
||||||
path: `${WALLET_DIRS[kind]}/${filename}`,
|
const preferred = await resolveWritableDirectory().catch(() => null)
|
||||||
directory: Directory.Documents,
|
const dirs = orderedDirectories(preferred, await directoryCandidates())
|
||||||
encoding: 'utf8',
|
let lastErr = null
|
||||||
})
|
for (const directory of dirs) {
|
||||||
|
try {
|
||||||
|
const { data } = await Filesystem.readFile({ path, directory, encoding: 'utf8' })
|
||||||
return JSON.parse(data)
|
return JSON.parse(data)
|
||||||
|
} catch (err) {
|
||||||
|
lastErr = err
|
||||||
|
if (!isMissingFileError(err)) throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastErr || new Error('Wallet file not found.')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Exported API ─────────────────────────────────────────────────────────────
|
// ── Exported API ─────────────────────────────────────────────────────────────
|
||||||
@@ -145,6 +238,8 @@ export const api = {
|
|||||||
|
|
||||||
getWalletDir: async () => {
|
getWalletDir: async () => {
|
||||||
if (isElectron) return window.electronAPI.getWalletDir()
|
if (isElectron) return window.electronAPI.getWalletDir()
|
||||||
return 'Documents/wallet-generator'
|
const { Directory } = await fs()
|
||||||
|
const directory = await resolveWritableDirectory()
|
||||||
|
return `${directoryLabel(directory, Directory)}/wallet-generator`
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user