chore(release): clean up repo for v0.0.1 release
Excluded from release bundle:
- CONTEXT.md, CHANGELOG.md (agent/project working notes)
- client-app/ (React Native messenger — tracked separately)
- contracts/hello_go/ (unused standalone example)
Kept contracts/counter/ and contracts/name_registry/ as vm-test fixtures
(referenced by vm/vm_test.go; NOT production contracts).
Docs refactor:
- docs/README.md — new top-level index with cross-references
- docs/quickstart.md — rewrite around single-node as primary path
- docs/node/README.md — full rewrite, all CLI flags, schema table
- docs/api/README.md — add /api/well-known-version, /api/update-check
- docs/contracts/README.md — split native (Go) vs WASM (user-deployable)
- docs/update-system.md — new, full 5-layer update system design
- README.md — link into docs/, drop CHANGELOG/client-app references
Build-time version system (inherited from earlier commits this branch):
- node --version / client --version with ldflags-injected metadata
- /api/well-known-version with {build, protocol_version, features[]}
- Peer-version gossip on dchain/version/v1
- /api/update-check against Gitea release API
- deploy/single/update.sh with semver guard + 15-min systemd jitter
This commit is contained in:
@@ -1,82 +0,0 @@
|
||||
/**
|
||||
* Create Account screen.
|
||||
* Generates a new Ed25519 + X25519 keypair and saves it securely.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, ScrollView, Alert } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { generateKeyFile } from '@/lib/crypto';
|
||||
import { saveKeyFile } from '@/lib/storage';
|
||||
import { useStore } from '@/lib/store';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
|
||||
export default function CreateAccountScreen() {
|
||||
const setKeyFile = useStore(s => s.setKeyFile);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function handleCreate() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const kf = generateKeyFile();
|
||||
await saveKeyFile(kf);
|
||||
setKeyFile(kf);
|
||||
router.replace('/(auth)/created');
|
||||
} catch (e: any) {
|
||||
Alert.alert('Error', e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
className="flex-1 bg-background"
|
||||
contentContainerClassName="px-6 pt-16 pb-10"
|
||||
>
|
||||
{/* Header */}
|
||||
<Button variant="ghost" size="sm" onPress={() => router.back()} className="self-start mb-6 -ml-2">
|
||||
← Back
|
||||
</Button>
|
||||
|
||||
<Text className="text-white text-3xl font-bold mb-2">Create Account</Text>
|
||||
<Text className="text-muted text-base mb-8 leading-6">
|
||||
A new identity will be generated on your device.
|
||||
Your private key never leaves this app.
|
||||
</Text>
|
||||
|
||||
{/* Info cards */}
|
||||
<Card className="mb-4 gap-3">
|
||||
<InfoRow icon="🔑" label="Ed25519 signing key" desc="Your blockchain address and tx signing key" />
|
||||
<InfoRow icon="🔒" label="X25519 encryption key" desc="End-to-end encryption for messages" />
|
||||
<InfoRow icon="📱" label="Stored on device" desc="Keys are encrypted in the device secure store" />
|
||||
</Card>
|
||||
|
||||
<Card className="mb-8 border-primary/30 bg-primary/10">
|
||||
<Text className="text-accent text-sm font-semibold mb-1">⚠ Important</Text>
|
||||
<Text className="text-muted text-sm leading-5">
|
||||
After creation, export and backup your key file.
|
||||
If you lose it there is no recovery — the blockchain has no password reset.
|
||||
</Text>
|
||||
</Card>
|
||||
|
||||
<Button onPress={handleCreate} loading={loading} size="lg">
|
||||
Generate Keys & Create Account
|
||||
</Button>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoRow({ icon, label, desc }: { icon: string; label: string; desc: string }) {
|
||||
return (
|
||||
<View className="flex-row items-start gap-3">
|
||||
<Text className="text-xl">{icon}</Text>
|
||||
<View className="flex-1">
|
||||
<Text className="text-white text-sm font-semibold">{label}</Text>
|
||||
<Text className="text-muted text-xs mt-0.5">{desc}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
/**
|
||||
* Account Created confirmation screen.
|
||||
* Shows address, pubkeys, and export options.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, ScrollView, Alert, Share } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import * as Clipboard from 'expo-clipboard';
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import * as Sharing from 'expo-sharing';
|
||||
import { useStore } from '@/lib/store';
|
||||
import { shortAddr } from '@/lib/crypto';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Separator } from '@/components/ui/Separator';
|
||||
|
||||
export default function AccountCreatedScreen() {
|
||||
const keyFile = useStore(s => s.keyFile);
|
||||
const [copied, setCopied] = useState<string | null>(null);
|
||||
|
||||
if (!keyFile) {
|
||||
router.replace('/');
|
||||
return null;
|
||||
}
|
||||
|
||||
async function copy(value: string, label: string) {
|
||||
await Clipboard.setStringAsync(value);
|
||||
setCopied(label);
|
||||
setTimeout(() => setCopied(null), 2000);
|
||||
}
|
||||
|
||||
async function exportKey() {
|
||||
try {
|
||||
const json = JSON.stringify(keyFile, null, 2);
|
||||
const path = FileSystem.cacheDirectory + 'dchain_key.json';
|
||||
await FileSystem.writeAsStringAsync(path, json);
|
||||
if (await Sharing.isAvailableAsync()) {
|
||||
await Sharing.shareAsync(path, {
|
||||
mimeType: 'application/json',
|
||||
dialogTitle: 'Save your DChain key file',
|
||||
});
|
||||
} else {
|
||||
Alert.alert('Export', 'Sharing not available on this device.');
|
||||
}
|
||||
} catch (e: any) {
|
||||
Alert.alert('Export failed', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
className="flex-1 bg-background"
|
||||
contentContainerClassName="px-6 pt-16 pb-10"
|
||||
>
|
||||
{/* Success header */}
|
||||
<View className="items-center mb-8">
|
||||
<View className="w-20 h-20 rounded-full bg-success/20 items-center justify-center mb-4">
|
||||
<Text className="text-4xl">✓</Text>
|
||||
</View>
|
||||
<Text className="text-white text-2xl font-bold">Account Created!</Text>
|
||||
<Text className="text-muted text-sm mt-2 text-center">
|
||||
Your keys have been generated and stored securely.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Address card */}
|
||||
<Card className="mb-4">
|
||||
<Text className="text-muted text-xs uppercase tracking-widest mb-3 font-semibold">
|
||||
Your Address (Ed25519)
|
||||
</Text>
|
||||
<Text className="text-white font-mono text-xs leading-5 mb-3">
|
||||
{keyFile.pub_key}
|
||||
</Text>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onPress={() => copy(keyFile.pub_key, 'address')}
|
||||
>
|
||||
{copied === 'address' ? '✓ Copied' : 'Copy Address'}
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
{/* X25519 key */}
|
||||
<Card className="mb-4">
|
||||
<Text className="text-muted text-xs uppercase tracking-widest mb-3 font-semibold">
|
||||
Encryption Key (X25519)
|
||||
</Text>
|
||||
<Text className="text-white font-mono text-xs leading-5 mb-3">
|
||||
{keyFile.x25519_pub}
|
||||
</Text>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onPress={() => copy(keyFile.x25519_pub, 'x25519')}
|
||||
>
|
||||
{copied === 'x25519' ? '✓ Copied' : 'Copy Encryption Key'}
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
{/* Export warning */}
|
||||
<Card className="mb-8 border-yellow-500/30 bg-yellow-500/10">
|
||||
<Text className="text-yellow-400 text-sm font-semibold mb-2">🔐 Backup your key file</Text>
|
||||
<Text className="text-muted text-xs leading-5 mb-3">
|
||||
Export <Text className="text-white font-mono">dchain_key.json</Text> and store it safely.
|
||||
This file contains your private keys — keep it secret.
|
||||
</Text>
|
||||
<Button variant="outline" onPress={exportKey}>
|
||||
Export key.json
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Button size="lg" onPress={() => router.replace('/(app)/chats')}>
|
||||
Open Messenger
|
||||
</Button>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
/**
|
||||
* Import Existing Key screen.
|
||||
* Two methods:
|
||||
* 1. Paste JSON directly into a text field
|
||||
* 2. Pick key.json file via document picker
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View, Text, ScrollView, TextInput,
|
||||
TouchableOpacity, Alert, Pressable,
|
||||
} from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
import * as Clipboard from 'expo-clipboard';
|
||||
import { saveKeyFile } from '@/lib/storage';
|
||||
import { useStore } from '@/lib/store';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import type { KeyFile } from '@/lib/types';
|
||||
|
||||
type Tab = 'paste' | 'file';
|
||||
|
||||
const REQUIRED_FIELDS: (keyof KeyFile)[] = ['pub_key', 'priv_key', 'x25519_pub', 'x25519_priv'];
|
||||
|
||||
function validateKeyFile(raw: string): KeyFile {
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = JSON.parse(raw.trim());
|
||||
} catch {
|
||||
throw new Error('Invalid JSON — check that you copied the full key file contents.');
|
||||
}
|
||||
for (const field of REQUIRED_FIELDS) {
|
||||
if (!parsed[field] || typeof parsed[field] !== 'string') {
|
||||
throw new Error(`Missing or invalid field: "${field}"`);
|
||||
}
|
||||
if (!/^[0-9a-f]+$/i.test(parsed[field])) {
|
||||
throw new Error(`Field "${field}" must be a hex string.`);
|
||||
}
|
||||
}
|
||||
return parsed as KeyFile;
|
||||
}
|
||||
|
||||
export default function ImportKeyScreen() {
|
||||
const setKeyFile = useStore(s => s.setKeyFile);
|
||||
|
||||
const [tab, setTab] = useState<Tab>('paste');
|
||||
const [jsonText, setJsonText] = useState('');
|
||||
const [fileName, setFileName] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// ── Shared: save validated key and navigate ──────────────────────────────
|
||||
async function applyKey(kf: KeyFile) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await saveKeyFile(kf);
|
||||
setKeyFile(kf);
|
||||
router.replace('/(app)/chats');
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Method 1: paste JSON ─────────────────────────────────────────────────
|
||||
async function handlePasteImport() {
|
||||
setError(null);
|
||||
const text = jsonText.trim();
|
||||
if (!text) {
|
||||
// Try reading clipboard if field is empty
|
||||
const clip = await Clipboard.getStringAsync();
|
||||
if (clip) setJsonText(clip);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const kf = validateKeyFile(text);
|
||||
await applyKey(kf);
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Method 2: pick file ──────────────────────────────────────────────────
|
||||
async function pickFile() {
|
||||
setError(null);
|
||||
try {
|
||||
const result = await DocumentPicker.getDocumentAsync({
|
||||
type: ['application/json', 'text/plain', '*/*'],
|
||||
copyToCacheDirectory: true,
|
||||
});
|
||||
if (result.canceled) return;
|
||||
|
||||
const asset = result.assets[0];
|
||||
setFileName(asset.name);
|
||||
|
||||
// Use fetch() — readAsStringAsync is deprecated in newer expo-file-system
|
||||
const response = await fetch(asset.uri);
|
||||
const raw = await response.text();
|
||||
|
||||
const kf = validateKeyFile(raw);
|
||||
await applyKey(kf);
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
const tabStyle = (t: Tab) => ({
|
||||
flex: 1 as const,
|
||||
paddingVertical: 10,
|
||||
alignItems: 'center' as const,
|
||||
borderBottomWidth: 2,
|
||||
borderBottomColor: tab === t ? '#2563eb' : 'transparent',
|
||||
});
|
||||
|
||||
const tabTextStyle = (t: Tab) => ({
|
||||
fontSize: 14,
|
||||
fontWeight: '600' as const,
|
||||
color: tab === t ? '#fff' : '#8b949e',
|
||||
});
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={{ flex: 1, backgroundColor: '#0d1117' }}
|
||||
contentContainerStyle={{ paddingHorizontal: 20, paddingTop: 60, paddingBottom: 40 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="on-drag"
|
||||
>
|
||||
{/* Back */}
|
||||
<Pressable onPress={() => router.back()} style={{ marginBottom: 24, alignSelf: 'flex-start' }}>
|
||||
<Text style={{ color: '#2563eb', fontSize: 15 }}>← Back</Text>
|
||||
</Pressable>
|
||||
|
||||
<Text style={{ color: '#fff', fontSize: 28, fontWeight: '700', marginBottom: 6 }}>
|
||||
Import Key
|
||||
</Text>
|
||||
<Text style={{ color: '#8b949e', fontSize: 15, lineHeight: 22, marginBottom: 24 }}>
|
||||
Restore your account from an existing{' '}
|
||||
<Text style={{ color: '#fff', fontFamily: 'monospace' }}>key.json</Text>.
|
||||
</Text>
|
||||
|
||||
{/* Tabs */}
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1, borderBottomColor: '#30363d',
|
||||
marginBottom: 24,
|
||||
}}>
|
||||
<TouchableOpacity style={tabStyle('paste')} onPress={() => setTab('paste')}>
|
||||
<Text style={tabTextStyle('paste')}>📋 Paste JSON</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={tabStyle('file')} onPress={() => setTab('file')}>
|
||||
<Text style={tabTextStyle('file')}>📁 Open File</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* ── Paste tab ── */}
|
||||
{tab === 'paste' && (
|
||||
<View style={{ gap: 12 }}>
|
||||
<Text style={{ color: '#8b949e', fontSize: 12, fontWeight: '600',
|
||||
textTransform: 'uppercase', letterSpacing: 1 }}>
|
||||
Key JSON
|
||||
</Text>
|
||||
|
||||
<View style={{
|
||||
backgroundColor: '#161b22',
|
||||
borderWidth: 1,
|
||||
borderColor: error ? '#f85149' : jsonText ? '#2563eb' : '#30363d',
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
}}>
|
||||
<TextInput
|
||||
value={jsonText}
|
||||
onChangeText={t => { setJsonText(t); setError(null); }}
|
||||
placeholder={'{\n "pub_key": "...",\n "priv_key": "...",\n "x25519_pub": "...",\n "x25519_priv": "..."\n}'}
|
||||
placeholderTextColor="#8b949e"
|
||||
multiline
|
||||
numberOfLines={8}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
style={{
|
||||
color: '#fff',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
lineHeight: 18,
|
||||
minHeight: 160,
|
||||
textAlignVertical: 'top',
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Paste from clipboard shortcut */}
|
||||
{!jsonText && (
|
||||
<TouchableOpacity
|
||||
onPress={async () => {
|
||||
const clip = await Clipboard.getStringAsync();
|
||||
if (clip) { setJsonText(clip); setError(null); }
|
||||
else Alert.alert('Clipboard empty', 'Copy your key JSON first.');
|
||||
}}
|
||||
style={{
|
||||
flexDirection: 'row', alignItems: 'center', gap: 8,
|
||||
padding: 12, backgroundColor: '#161b22',
|
||||
borderWidth: 1, borderColor: '#30363d', borderRadius: 12,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 18 }}>📋</Text>
|
||||
<Text style={{ color: '#8b949e', fontSize: 14 }}>Paste from clipboard</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<View style={{
|
||||
backgroundColor: 'rgba(248,81,73,0.1)',
|
||||
borderWidth: 1, borderColor: 'rgba(248,81,73,0.3)',
|
||||
borderRadius: 10, padding: 12,
|
||||
}}>
|
||||
<Text style={{ color: '#f85149', fontSize: 13, lineHeight: 18 }}>⚠ {error}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
loading={loading}
|
||||
disabled={!jsonText.trim()}
|
||||
onPress={handlePasteImport}
|
||||
>
|
||||
Import Key
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* ── File tab ── */}
|
||||
{tab === 'file' && (
|
||||
<View style={{ gap: 12 }}>
|
||||
<TouchableOpacity
|
||||
onPress={pickFile}
|
||||
style={{
|
||||
backgroundColor: '#161b22',
|
||||
borderWidth: 2, borderColor: '#30363d',
|
||||
borderRadius: 16, borderStyle: 'dashed',
|
||||
padding: 32, alignItems: 'center', gap: 12,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 40 }}>📂</Text>
|
||||
<Text style={{ color: '#fff', fontSize: 16, fontWeight: '600' }}>
|
||||
{fileName ?? 'Choose key.json'}
|
||||
</Text>
|
||||
<Text style={{ color: '#8b949e', fontSize: 13, textAlign: 'center' }}>
|
||||
Tap to browse files
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{fileName && (
|
||||
<View style={{
|
||||
flexDirection: 'row', alignItems: 'center', gap: 10,
|
||||
backgroundColor: 'rgba(63,185,80,0.1)',
|
||||
borderWidth: 1, borderColor: 'rgba(63,185,80,0.3)',
|
||||
borderRadius: 10, padding: 12,
|
||||
}}>
|
||||
<Text style={{ fontSize: 18 }}>📄</Text>
|
||||
<Text style={{ color: '#3fb950', fontSize: 13, flex: 1 }} numberOfLines={1}>
|
||||
{fileName}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<View style={{
|
||||
backgroundColor: 'rgba(248,81,73,0.1)',
|
||||
borderWidth: 1, borderColor: 'rgba(248,81,73,0.3)',
|
||||
borderRadius: 10, padding: 12,
|
||||
}}>
|
||||
<Text style={{ color: '#f85149', fontSize: 13, lineHeight: 18 }}>⚠ {error}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<Text style={{ color: '#8b949e', textAlign: 'center', fontSize: 14 }}>
|
||||
Validating key…
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Format hint */}
|
||||
<View style={{
|
||||
marginTop: 28, padding: 14,
|
||||
backgroundColor: '#161b22',
|
||||
borderWidth: 1, borderColor: '#30363d', borderRadius: 12,
|
||||
}}>
|
||||
<Text style={{ color: '#8b949e', fontSize: 12, fontWeight: '600',
|
||||
marginBottom: 8, textTransform: 'uppercase', letterSpacing: 1 }}>
|
||||
Expected format
|
||||
</Text>
|
||||
<Text style={{ color: '#8b949e', fontFamily: 'monospace', fontSize: 11, lineHeight: 17 }}>
|
||||
{`{\n "pub_key": "<64 hex chars>",\n "priv_key": "<128 hex chars>",\n "x25519_pub": "<64 hex chars>",\n "x25519_priv": "<64 hex chars>"\n}`}
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user