diff --git a/app/(tabs)/library/programs.tsx b/app/(tabs)/library/programs.tsx index 4ebe7d5..2407660 100644 --- a/app/(tabs)/library/programs.tsx +++ b/app/(tabs)/library/programs.tsx @@ -1,102 +1,42 @@ // app/(tabs)/library/programs.tsx -import React, { useState, useEffect, useRef } from 'react'; -import { View, ScrollView, TextInput, ActivityIndicator, Platform, TouchableOpacity } from 'react-native'; +import React, { useState, useEffect } from 'react'; +import { View, ScrollView, TextInput, ActivityIndicator, Platform, TouchableOpacity, Modal } from 'react-native'; import { Text } from '@/components/ui/text'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { AlertCircle, CheckCircle2, Database, RefreshCcw, Trash2, - Code, Search, ListFilter, Wifi, Zap, FileJson + Code, Search, ListFilter, Wifi, Zap, FileJson, X, Info } from 'lucide-react-native'; import { useSQLiteContext } from 'expo-sqlite'; -import { ExerciseType, ExerciseCategory, Equipment } from '@/types/exercise'; -import { SQLTransaction, SQLResultSet, SQLError } from '@/lib/db/types'; +import { useNDK, useNDKAuth, useNDKCurrentUser } from '@/lib/hooks/useNDK'; import { schema } from '@/lib/db/schema'; import { FilterSheet, type FilterOptions, type SourceType } from '@/components/library/FilterSheet'; -import { Switch } from '@/components/ui/switch'; -import { Label } from '@/components/ui/label'; import { Separator } from '@/components/ui/separator'; -import { getPublicKey } from 'nostr-tools'; +import { NostrEventKind } from '@/types/nostr'; +import { useNDKStore } from '@/lib/stores/ndk'; -// Constants for Nostr -const EVENT_KIND_EXERCISE = 33401; -const EVENT_KIND_WORKOUT_TEMPLATE = 33402; -const EVENT_KIND_WORKOUT_RECORD = 1301; +// Define relay status +enum RelayStatus { + DISCONNECTED = 'disconnected', + CONNECTING = 'connecting', + CONNECTED = 'connected', + ERROR = 'error' +} -// Simplified mock implementations for testing -const generatePrivateKey = (): string => { - // Generate a random hex string (32 bytes) - return Array.from({ length: 64 }, () => - Math.floor(Math.random() * 16).toString(16) - ).join(''); -}; - -const getEventHash = (event: any): string => { - // For testing, just create a mock hash - const eventData = JSON.stringify([ - 0, - event.pubkey, - event.created_at, - event.kind, - event.tags, - event.content - ]); - - // Simple hash function for demonstration - return Array.from(eventData) - .reduce((hash, char) => { - return ((hash << 5) - hash) + char.charCodeAt(0); - }, 0) - .toString(16) - .padStart(64, '0'); -}; - -const signEvent = (event: any, privateKey: string): string => { - // In real implementation, this would sign the event hash with the private key - // For testing, we'll just return a mock signature - return Array.from({ length: 128 }, () => - Math.floor(Math.random() * 16).toString(16) - ).join(''); -}; - -interface NostrEvent { - id?: string; +// Interface for event display +interface DisplayEvent { + id: string; pubkey: string; - created_at: number; kind: number; - tags: string[][]; + created_at: number; content: string; + tags: string[][]; sig?: string; } -interface TableInfo { - name: string; -} - -interface TableSchema { - name: string; - sql: string; -} - -interface SchemaVersion { - version: number; -} - -interface ExerciseRow { - id: string; - title: string; - type: string; - category: string; - equipment: string | null; - description: string | null; - created_at: number; - updated_at: number; - format_json: string; - format_units_json: string; -} - -// Default available filters for programs - can be adjusted later +// Default available filters for programs const availableFilters = { equipment: ['Barbell', 'Dumbbell', 'Bodyweight', 'Machine', 'Cables', 'Other'], tags: ['Strength', 'Cardio', 'Mobility', 'Recovery'], @@ -121,7 +61,7 @@ export default function ProgramsScreen() { initialized: false, tables: [], }); - const [schemas, setSchemas] = useState([]); + const [schemas, setSchemas] = useState<{name: string, sql: string}[]>([]); const [testResults, setTestResults] = useState<{ success: boolean; message: string; @@ -132,35 +72,48 @@ export default function ProgramsScreen() { const [activeFilters, setActiveFilters] = useState(0); // Nostr state - const [relayUrl, setRelayUrl] = useState('ws://localhost:7777'); - const [connected, setConnected] = useState(false); - const [connecting, setConnecting] = useState(false); + const [relayStatus, setRelayStatus] = useState(RelayStatus.DISCONNECTED); const [statusMessage, setStatusMessage] = useState(''); - const [events, setEvents] = useState([]); + const [events, setEvents] = useState([]); const [loading, setLoading] = useState(false); - const [privateKey, setPrivateKey] = useState(''); - const [publicKey, setPublicKey] = useState(''); - const [useGeneratedKeys, setUseGeneratedKeys] = useState(true); - const [eventKind, setEventKind] = useState(EVENT_KIND_EXERCISE); + const [eventKind, setEventKind] = useState(NostrEventKind.EXERCISE); const [eventContent, setEventContent] = useState(''); + const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false); + const [privateKey, setPrivateKey] = useState(''); + const [error, setError] = useState(null); - // WebSocket reference - const socketRef = useRef(null); + // Use the NDK hooks + const { ndk, isLoading: ndkLoading } = useNDK(); + const { currentUser, isAuthenticated } = useNDKCurrentUser(); + const { login, logout, generateKeys } = useNDKAuth(); // Tab state const [activeTab, setActiveTab] = useState('database'); - useEffect(() => { + // Check database status checkDatabase(); inspectDatabase(); - generateKeys(); - }, []); + + // Update relay status when NDK changes + if (ndk) { + setRelayStatus(RelayStatus.CONNECTED); + setStatusMessage(isAuthenticated + ? `Connected as ${currentUser?.npub?.slice(0, 8)}...` + : 'Connected to relays via NDK'); + } else if (ndkLoading) { + setRelayStatus(RelayStatus.CONNECTING); + setStatusMessage('Connecting to relays...'); + } else { + setRelayStatus(RelayStatus.DISCONNECTED); + setStatusMessage('Not connected'); + } + }, [ndk, ndkLoading, isAuthenticated, currentUser]); // DATABASE FUNCTIONS const inspectDatabase = async () => { try { - const result = await db.getAllAsync( + const result = await db.getAllAsync<{name: string, sql: string}>( "SELECT name, sql FROM sqlite_master WHERE type='table'" ); setSchemas(result); @@ -172,12 +125,12 @@ export default function ProgramsScreen() { const checkDatabase = async () => { try { // Check schema_version table - const version = await db.getFirstAsync( + const version = await db.getFirstAsync<{version: number}>( 'SELECT version FROM schema_version ORDER BY version DESC LIMIT 1' ); // Get all tables - const tables = await db.getAllAsync( + const tables = await db.getAllAsync<{name: string}>( "SELECT name FROM sqlite_master WHERE type='table'" ); @@ -193,6 +146,7 @@ export default function ProgramsScreen() { })); } }; + const resetDatabase = async () => { try { await db.withTransactionAsync(async () => { @@ -231,9 +185,9 @@ export default function ProgramsScreen() { // Test exercise const testExercise = { title: "Test Squat", - type: "strength" as ExerciseType, - category: "Legs" as ExerciseCategory, - equipment: "barbell" as Equipment, + type: "strength", + category: "Legs", + equipment: "barbell", description: "Test exercise", tags: ["test", "legs"], format: { @@ -241,8 +195,8 @@ export default function ProgramsScreen() { reps: true }, format_units: { - weight: "kg" as const, - reps: "count" as const + weight: "kg", + reps: "count" } }; @@ -280,7 +234,7 @@ export default function ProgramsScreen() { }); // Verify insert - const result = await db.getFirstAsync( + const result = await db.getFirstAsync( "SELECT * FROM exercises WHERE id = ?", ['test-1'] ); @@ -308,184 +262,216 @@ export default function ProgramsScreen() { setActiveFilters(totalFilters); // Implement filtering logic for programs when available }; - // NOSTR FUNCTIONS + + // Handle login dialog + const handleShowLogin = () => { + setIsLoginSheetOpen(true); + }; - // Generate new keypair - const generateKeys = () => { + // Close login sheet + const handleCloseLogin = () => { + setIsLoginSheetOpen(false); + }; + + // Handle key generation + const handleGenerateKeys = async () => { try { - const privKey = generatePrivateKey(); - // For getPublicKey, we can use a mock function that returns a valid-looking pubkey - const pubKey = privKey.slice(0, 64); // Just use part of the private key for demo - - setPrivateKey(privKey); - setPublicKey(pubKey); - setStatusMessage('Keys generated successfully'); - } catch (error) { - setStatusMessage(`Error generating keys: ${error}`); + const { nsec } = generateKeys(); + setPrivateKey(nsec); + setError(null); + } catch (err) { + setError('Failed to generate keys'); + console.error('Key generation error:', err); } }; - - // Connect to relay - const connectToRelay = () => { - if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) { - socketRef.current.close(); - } - - setConnecting(true); - setStatusMessage('Connecting to relay...'); - - try { - const socket = new WebSocket(relayUrl); - - socket.onopen = () => { - setConnected(true); - setConnecting(false); - setStatusMessage('Connected to relay!'); - socketRef.current = socket; - - // Subscribe to exercise-related events - const subscriptionId = 'test-sub-' + Math.random().toString(36).substring(2, 15); - const subscription = JSON.stringify([ - 'REQ', - subscriptionId, - { kinds: [EVENT_KIND_EXERCISE, EVENT_KIND_WORKOUT_TEMPLATE, EVENT_KIND_WORKOUT_RECORD], limit: 10 } - ]); - socket.send(subscription); - }; - - socket.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - if (data[0] === 'EVENT' && data[1] && data[2]) { - const nostrEvent = data[2]; - setEvents(prev => [nostrEvent, ...prev].slice(0, 50)); // Keep most recent 50 events - } else if (data[0] === 'NOTICE') { - setStatusMessage(`Relay message: ${data[1]}`); - } - } catch (error) { - console.error('Error parsing message:', error, event.data); - } - }; - - socket.onclose = () => { - setConnected(false); - setConnecting(false); - setStatusMessage('Disconnected from relay'); - }; - - socket.onerror = (error) => { - setConnected(false); - setConnecting(false); - setStatusMessage(`Connection error: ${error}`); - console.error('WebSocket error:', error); - }; - } catch (error) { - setConnecting(false); - setStatusMessage(`Failed to connect: ${error}`); - console.error('Connection setup error:', error); - } - }; - - // Disconnect from relay - const disconnectFromRelay = () => { - if (socketRef.current) { - socketRef.current.close(); - } - }; - - // Create and publish a new event - const publishEvent = () => { - if (!socketRef.current || socketRef.current.readyState !== WebSocket.OPEN) { - setStatusMessage('Not connected to a relay'); + + // Handle login + const handleLogin = async () => { + if (!privateKey.trim()) { + setError('Please enter your private key or generate a new one'); return; } - - if (!privateKey || !publicKey) { - setStatusMessage('Need private and public keys to publish'); - return; + + setError(null); + try { + const success = await login(privateKey); + + if (success) { + setPrivateKey(''); + handleCloseLogin(); + } else { + setError('Failed to login with the provided key'); + } + } catch (err) { + console.error('Login error:', err); + setError(err instanceof Error ? err.message : 'An unexpected error occurred'); } - + }; + + // Handle logout + const handleLogout = async () => { try { setLoading(true); + await logout(); + setStatusMessage('Logged out'); + setEvents([]); + } catch (error) { + console.error('Logout error:', error); + setStatusMessage(`Logout error: ${error instanceof Error ? error.message : 'Unknown error'}`); + } finally { + setLoading(false); + } + }; + + // Publish an event + const handlePublishEvent = async () => { + if (!isAuthenticated || !ndk || !currentUser) { + setStatusMessage('You must login first'); + return; + } + + setLoading(true); + + try { + console.log('Creating event...'); - // Create event with required pubkey (no longer optional) - const event: NostrEvent = { - pubkey: publicKey, - created_at: Math.floor(Date.now() / 1000), - kind: eventKind, - tags: [], - content: eventContent, - }; + // Prepare tags based on event kind + const tags: string[][] = []; + const timestamp = Math.floor(Date.now() / 1000); - // A basic implementation for each event kind - if (eventKind === EVENT_KIND_EXERCISE) { - event.tags.push(['d', `exercise-${Date.now()}`]); - event.tags.push(['title', 'Test Exercise']); - event.tags.push(['format', 'weight', 'reps']); - event.tags.push(['format_units', 'kg', 'count']); - event.tags.push(['equipment', 'barbell']); - } else if (eventKind === EVENT_KIND_WORKOUT_TEMPLATE) { - event.tags.push(['d', `template-${Date.now()}`]); - event.tags.push(['title', 'Test Workout Template']); - event.tags.push(['type', 'strength']); - } else if (eventKind === EVENT_KIND_WORKOUT_RECORD) { - event.tags.push(['d', `workout-${Date.now()}`]); - event.tags.push(['title', 'Test Workout Record']); - event.tags.push(['start', `${Math.floor(Date.now() / 1000) - 3600}`]); - event.tags.push(['end', `${Math.floor(Date.now() / 1000)}`]); + // Add appropriate tags based on event kind + if (eventKind === NostrEventKind.TEXT) { + // For regular text notes, we can add some simple tags + tags.push( + ['t', 'powr'], // Adding a hashtag + ['t', 'test'], // Another hashtag + ['client', 'POWR App'] // Client info + ); + + // If no content was provided, use a default message + if (!eventContent || eventContent.trim() === '') { + setEventContent('Hello from POWR App - Test Note'); + } + } else if (eventKind === NostrEventKind.EXERCISE) { + // Your existing exercise event code + const uniqueId = `exercise-${timestamp}`; + tags.push( + ['d', uniqueId], + ['title', eventContent || 'Test Exercise'], + // Rest of your tags... + ); + } + if (eventKind === NostrEventKind.EXERCISE) { + const uniqueId = `exercise-${timestamp}`; + tags.push( + ['d', uniqueId], + ['title', eventContent || 'Test Exercise'], + ['type', 'strength'], + ['category', 'Legs'], + ['format', 'weight', 'reps'], + ['format_units', 'kg', 'count'], + ['equipment', 'barbell'], + ['t', 'test'], + ['t', 'powr'] + ); + } else if (eventKind === NostrEventKind.TEMPLATE) { + const uniqueId = `template-${timestamp}`; + tags.push( + ['d', uniqueId], + ['title', eventContent || 'Test Workout Template'], + ['type', 'strength'], + ['t', 'strength'], + ['t', 'legs'], + ['t', 'powr'], + // Add exercise references - these would normally reference real exercise events + ['exercise', `33401:exercise-${timestamp-1}`, '3', '10', 'normal'] + ); + } else if (eventKind === NostrEventKind.WORKOUT) { + const uniqueId = `workout-${timestamp}`; + const startTime = timestamp - 3600; // 1 hour ago + tags.push( + ['d', uniqueId], + ['title', eventContent || 'Test Workout Record'], + ['start', `${startTime}`], + ['end', `${timestamp}`], + ['completed', 'true'], + ['t', 'powr'], + // Add exercise data - these would normally reference real exercise events + ['exercise', `33401:exercise-${timestamp-1}`, '100', '10', '8', 'normal'] + ); } - // Hash and sign - event.id = getEventHash(event); - event.sig = signEvent(event, privateKey); + // Use the NDK store's publishEvent function + const event = await useNDKStore.getState().publishEvent(eventKind, eventContent, tags); - // Publish to relay - const message = JSON.stringify(['EVENT', event]); - socketRef.current.send(message); - - setStatusMessage('Event published successfully!'); - setEventContent(''); - setLoading(false); + if (event) { + // Add the published event to our display list + const displayEvent: DisplayEvent = { + id: event.id || '', + pubkey: event.pubkey, + kind: event.kind || eventKind, // Add fallback to eventKind if kind is undefined + created_at: event.created_at || Math.floor(Date.now() / 1000), // Add fallback timestamp + content: event.content, + tags: event.tags.map(tag => tag.map(item => String(item))), + sig: event.sig + }; + + setEvents(prev => [displayEvent, ...prev]); + + // Clear content field + setEventContent(''); + setStatusMessage('Event published successfully!'); + } else { + setStatusMessage('Failed to publish event'); + } } catch (error) { - setStatusMessage(`Error publishing event: ${error}`); + console.error('Error publishing event:', error); + setStatusMessage(`Error publishing: ${error instanceof Error ? error.message : 'Unknown error'}`); + } finally { setLoading(false); } }; - // Query events from relay - const queryEvents = () => { - if (!socketRef.current || socketRef.current.readyState !== WebSocket.OPEN) { - setStatusMessage('Not connected to a relay'); + // Query events from NDK + const queryEvents = async () => { + if (!ndk) { + setStatusMessage('NDK not initialized'); return; } + setLoading(true); + setEvents([]); + try { - setEvents([]); - setLoading(true); + // Create a filter for the specific kind + const filter = { kinds: [eventKind as number], limit: 20 }; - // Create a new subscription for the selected event kind - const subscriptionId = 'query-' + Math.random().toString(36).substring(2, 15); - const subscription = JSON.stringify([ - 'REQ', - subscriptionId, - { kinds: [eventKind], limit: 20 } - ]); + // Use the NDK store's fetchEventsByFilter function + const fetchedEvents = await useNDKStore.getState().fetchEventsByFilter(filter); - socketRef.current.send(subscription); - setStatusMessage(`Querying events of kind ${eventKind}...`); - - // Close this subscription after 5 seconds - setTimeout(() => { - if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) { - socketRef.current.send(JSON.stringify(['CLOSE', subscriptionId])); - setLoading(false); - setStatusMessage(`Completed query for kind ${eventKind}`); - } - }, 5000); + const displayEvents: DisplayEvent[] = []; + fetchedEvents.forEach(event => { + // Ensure we handle potentially undefined values + displayEvents.push({ + id: event.id || '', + pubkey: event.pubkey, + kind: event.kind || eventKind, // Use eventKind as fallback + created_at: event.created_at || Math.floor(Date.now() / 1000), // Use current time as fallback + content: event.content, + // Convert tags to string[][] + tags: event.tags.map(tag => tag.map(item => String(item))), + sig: event.sig + }); + }); + + setEvents(displayEvents); + setStatusMessage(`Fetched ${displayEvents.length} events`); } catch (error) { + console.error('Error querying events:', error); + setStatusMessage(`Error querying: ${error instanceof Error ? error.message : 'Unknown error'}`); + } finally { setLoading(false); - setStatusMessage(`Error querying events: ${error}`); } }; return ( @@ -548,7 +534,6 @@ export default function ProgramsScreen() { Nostr - {/* Tab Content */} {activeTab === 'database' && ( @@ -688,102 +673,136 @@ export default function ProgramsScreen() { Nostr Integration Test - {/* Connection controls */} + {/* Connection status and controls */} - Relay Connection + Nostr Connection - - - + - - Status: {connected ? 'Connected' : 'Disconnected'} + + Status: {relayStatus} {statusMessage ? ( - {statusMessage} + {statusMessage} ) : null} + + {isAuthenticated && currentUser && ( + + Logged in as: + {currentUser.npub} + {currentUser.profile?.displayName && ( + {currentUser.profile.displayName} + )} + + {/* Display active relay */} + Active Relay: + wss://powr.duckdns.org + + Note: To publish to additional relays, uncomment them in stores/ndk.ts + + + )} - {/* Keys */} - - - - - Nostr Keys - - - - - - - + {/* Login Modal */} + + + + + Login with Nostr + + + + + + + Enter your Nostr private key (nsec) + + + {error && ( + + {error} + + )} + + + + + + + + + + + What is a Nostr Key? + + + Nostr is a decentralized protocol where your private key (nsec) is your identity and password. + Your private key is securely stored on your device and is never sent to any servers. + + + - - Public Key: - - - Private Key: - - Note: Never share your private key in a production app - - - + + {/* Create Event */} @@ -794,29 +813,37 @@ export default function ProgramsScreen() { Event Kind: - + + + @@ -832,8 +859,8 @@ export default function ProgramsScreen() { - - - - - - - What is a Nostr Key? + + + + Enter your Nostr private key (nsec) + + + {error && ( + + {error} + + )} + + + + + + + + + + + What is a Nostr Key? + + + Nostr is a decentralized protocol where your private key (nsec) is your identity and password. + + + Your private key is securely stored on your device and is never sent to any servers. + - - Nostr is a decentralized protocol where your private key (nsec) is your identity and password. - - - Your private key is securely stored on your device and is never sent to any servers. - - - - - - + + + + + ); } const styles = StyleSheet.create({ - container: { + modalOverlay: { flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + }, + modalContent: { + width: '90%', + maxWidth: 500, + backgroundColor: '#FFFFFF', + borderRadius: 12, + padding: 20, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 5, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 15, + }, + title: { + fontSize: 18, + fontWeight: 'bold', + }, + container: { + maxHeight: '80%', }, scrollView: { flex: 1, }, content: { - padding: 16, + padding: 10, }, + loader: { + marginRight: 8, + } }); \ No newline at end of file diff --git a/lib/crypto-polyfill.ts b/lib/crypto-polyfill.ts new file mode 100644 index 0000000..45e0ca2 --- /dev/null +++ b/lib/crypto-polyfill.ts @@ -0,0 +1,91 @@ +// lib/crypto-polyfill.ts +import 'react-native-get-random-values'; +import * as Crypto from 'expo-crypto'; + +// Set up a more reliable polyfill +export function setupCryptoPolyfill() { + console.log('Setting up crypto polyfill...'); + + // Instead of using Object.defineProperty, let's use a different approach + try { + // First check if crypto exists and has getRandomValues + if (typeof global.crypto === 'undefined') { + (global as any).crypto = {}; + } + + // Only define getRandomValues if it doesn't exist or isn't working + if (!global.crypto.getRandomValues) { + console.log('Defining getRandomValues implementation'); + (global.crypto as any).getRandomValues = function(array: Uint8Array) { + console.log('Custom getRandomValues called'); + try { + return Crypto.getRandomBytes(array.length); + } catch (e) { + console.error('Error in getRandomValues:', e); + throw e; + } + }; + } + + // Test if it works + const testArray = new Uint8Array(8); + try { + const result = global.crypto.getRandomValues(testArray); + console.log('Crypto polyfill test result:', !!result); + return true; + } catch (testError) { + console.error('Crypto test failed:', testError); + return false; + } + } catch (error) { + console.error('Error setting up crypto polyfill:', error); + return false; + } +} + +// Also expose a monkey-patching function for the specific libraries +export function monkeyPatchNostrLibraries() { + try { + console.log('Attempting to monkey-patch nostr libraries...'); + + // Direct monkey patching of the randomBytes function in nostr libraries + // This is an extreme approach but might be necessary + const customRandomBytes = function(length: number): Uint8Array { + console.log('Using custom randomBytes implementation'); + return Crypto.getRandomBytes(length); + }; + + // Try to locate and patch the randomBytes function + try { + // Try to access the module using require + const nobleHashes = require('@noble/hashes/utils'); + if (nobleHashes && nobleHashes.randomBytes) { + console.log('Patching @noble/hashes/utils randomBytes'); + (nobleHashes as any).randomBytes = customRandomBytes; + } + } catch (e) { + console.log('Could not patch @noble/hashes/utils:', e); + } + + // Also try to patch nostr-tools if available + try { + const nostrTools = require('nostr-tools'); + if (nostrTools && nostrTools.crypto && nostrTools.crypto.randomBytes) { + console.log('Patching nostr-tools crypto.randomBytes'); + (nostrTools.crypto as any).randomBytes = customRandomBytes; + } + } catch (e) { + console.log('Could not patch nostr-tools:', e); + } + + return true; + } catch (error) { + console.error('Error in monkey patching:', error); + return false; + } +} + +// Set up the polyfill +setupCryptoPolyfill(); +// Try monkey patching as well +monkeyPatchNostrLibraries(); \ No newline at end of file diff --git a/lib/hooks/useNDK.ts b/lib/hooks/useNDK.ts index d8a6640..ef4569e 100644 --- a/lib/hooks/useNDK.ts +++ b/lib/hooks/useNDK.ts @@ -1,10 +1,19 @@ // lib/hooks/useNDK.ts import { useEffect } from 'react'; -import { useNDKStore } from '../stores/ndk'; +import { useNDKStore } from '@/lib/stores/ndk'; import type { NDKUser } from '@nostr-dev-kit/ndk'; +/** + * Hook to access NDK instance and initialization status + */ export function useNDK() { - const { ndk, isLoading, init } = useNDKStore(); + const { ndk, isLoading, error, init, relayStatus } = useNDKStore(state => ({ + ndk: state.ndk, + isLoading: state.isLoading, + error: state.error, + init: state.init, + relayStatus: state.relayStatus + })); useEffect(() => { if (!ndk && !isLoading) { @@ -12,15 +21,27 @@ export function useNDK() { } }, [ndk, isLoading, init]); - return { ndk, isLoading }; + return { + ndk, + isLoading, + error, + relayStatus + }; } +/** + * Hook to access current NDK user information + */ export function useNDKCurrentUser(): { currentUser: NDKUser | null; isAuthenticated: boolean; isLoading: boolean; } { - const { currentUser, isAuthenticated, isLoading } = useNDKStore(); + const { currentUser, isAuthenticated, isLoading } = useNDKStore(state => ({ + currentUser: state.currentUser, + isAuthenticated: state.isAuthenticated, + isLoading: state.isLoading + })); return { currentUser, @@ -29,13 +50,38 @@ export function useNDKCurrentUser(): { }; } +/** + * Hook to access NDK authentication methods + */ export function useNDKAuth() { - const { login, logout, isAuthenticated, isLoading } = useNDKStore(); + const { login, logout, isAuthenticated, isLoading, generateKeys } = useNDKStore(state => ({ + login: state.login, + logout: state.logout, + isAuthenticated: state.isAuthenticated, + isLoading: state.isLoading, + generateKeys: state.generateKeys + })); return { login, logout, + generateKeys, isAuthenticated, isLoading }; +} + +/** + * Hook for direct access to Nostr event actions + */ +export function useNDKEvents() { + const { publishEvent, fetchEventsByFilter } = useNDKStore(state => ({ + publishEvent: state.publishEvent, + fetchEventsByFilter: state.fetchEventsByFilter + })); + + return { + publishEvent, + fetchEventsByFilter + }; } \ No newline at end of file diff --git a/lib/hooks/useSubscribe.ts b/lib/hooks/useSubscribe.ts index 3b907c3..fb3a1f7 100644 --- a/lib/hooks/useSubscribe.ts +++ b/lib/hooks/useSubscribe.ts @@ -1,55 +1,85 @@ // lib/hooks/useSubscribe.ts -import { useEffect, useState } from 'react'; -import { NDKEvent, NDKFilter, NDKSubscription } from '@nostr-dev-kit/ndk'; +import { useEffect, useState, useRef } from 'react'; +import { NDKFilter, NDKSubscription } from '@nostr-dev-kit/ndk'; +import { NDKEvent, NDKUser } from '@nostr-dev-kit/ndk-mobile'; import { useNDK } from './useNDK'; interface UseSubscribeOptions { enabled?: boolean; closeOnEose?: boolean; + deduplicate?: boolean; } +/** + * Hook to subscribe to Nostr events + * + * @param filters The NDK filter or array of filters + * @param options Optional configuration options + * @returns Object containing events, loading state, and EOSE status + */ export function useSubscribe( - filters: NDKFilter[] | false, + filters: NDKFilter | NDKFilter[] | false, options: UseSubscribeOptions = {} ) { const { ndk } = useNDK(); const [events, setEvents] = useState([]); const [isLoading, setIsLoading] = useState(true); const [eose, setEose] = useState(false); + const subscriptionRef = useRef(null); // Default options - const { enabled = true, closeOnEose = false } = options; + const { + enabled = true, + closeOnEose = false, + deduplicate = true + } = options; useEffect(() => { + // Clean up previous subscription if exists + if (subscriptionRef.current) { + subscriptionRef.current.stop(); + subscriptionRef.current = null; + } + + // Reset state when filters change + setEvents([]); + setEose(false); + + // Check prerequisites if (!ndk || !filters || !enabled) { setIsLoading(false); return; } setIsLoading(true); - setEose(false); - - let subscription: NDKSubscription; try { - subscription = ndk.subscribe(filters); + // Convert single filter to array if needed + const filterArray = Array.isArray(filters) ? filters : [filters]; + // Create subscription + const subscription = ndk.subscribe(filterArray); + subscriptionRef.current = subscription; + + // Handle incoming events subscription.on('event', (event: NDKEvent) => { setEvents(prev => { - // Avoid duplicates - if (prev.some(e => e.id === event.id)) { + // Deduplicate events if enabled + if (deduplicate && prev.some(e => e.id === event.id)) { return prev; } return [...prev, event]; }); }); + // Handle end of stored events subscription.on('eose', () => { setIsLoading(false); setEose(true); - if (closeOnEose) { - subscription.stop(); + if (closeOnEose && subscriptionRef.current) { + subscriptionRef.current.stop(); + subscriptionRef.current = null; } }); } catch (error) { @@ -57,12 +87,27 @@ export function useSubscribe( setIsLoading(false); } + // Cleanup function return () => { - if (subscription) { - subscription.stop(); + if (subscriptionRef.current) { + subscriptionRef.current.stop(); + subscriptionRef.current = null; } }; - }, [ndk, enabled, closeOnEose, JSON.stringify(filters)]); + }, [ndk, enabled, closeOnEose, deduplicate, JSON.stringify(filters)]); - return { events, isLoading, eose }; + return { + events, + isLoading, + eose, + resubscribe: () => { + if (subscriptionRef.current) { + subscriptionRef.current.stop(); + subscriptionRef.current = null; + } + setEvents([]); + setEose(false); + setIsLoading(true); + } + }; } \ No newline at end of file diff --git a/lib/mobile-signer.ts b/lib/mobile-signer.ts new file mode 100644 index 0000000..c5f2ec8 --- /dev/null +++ b/lib/mobile-signer.ts @@ -0,0 +1,89 @@ +// lib/mobile-signer.ts +import '../lib/crypto-polyfill'; // Import crypto polyfill first +import * as Crypto from 'expo-crypto'; +import * as Random from 'expo-random'; +import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; +import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; +import * as nostrTools from 'nostr-tools'; +import { setupCryptoPolyfill } from './crypto-polyfill'; + +/** + * A custom signer implementation for React Native + * Extends NDKPrivateKeySigner to handle different key formats + */ +export class NDKMobilePrivateKeySigner extends NDKPrivateKeySigner { + constructor(privateKey: string) { + // Handle different private key formats + let hexKey = privateKey; + + // Convert nsec to hex if needed + if (privateKey.startsWith('nsec')) { + try { + const { type, data } = nostrTools.nip19.decode(privateKey); + if (type === 'nsec') { + // Handle the data as string (already in hex format) + if (typeof data === 'string') { + hexKey = data; + } + // Handle if it's a Uint8Array + else if (data instanceof Uint8Array) { + hexKey = bytesToHex(data); + } + } else { + throw new Error('Not an nsec key'); + } + } catch (e) { + console.error('Error processing nsec key:', e); + throw new Error('Invalid private key format'); + } + } + + // Call the parent constructor with the hex key + super(hexKey); + } +} + +/** + * Generate a new Nostr keypair + * Uses Expo's crypto functions directly instead of relying on polyfills + */ +// Add this to your generateKeyPair function +export function generateKeyPair() { + try { + // Ensure crypto polyfill is set up + if (typeof setupCryptoPolyfill === 'function') { + setupCryptoPolyfill(); + } + + let privateKeyBytes; + + // Try expo-crypto first since expo-random is deprecated + try { + privateKeyBytes = Crypto.getRandomBytes(32); + } catch (e) { + console.warn('expo-crypto failed:', e); + // Fallback to expo-random as last resort + privateKeyBytes = Random.getRandomBytes(32); + } + + const privateKey = bytesToHex(privateKeyBytes); + + // Get the public key from the private key using nostr-tools + const publicKey = nostrTools.getPublicKey(privateKeyBytes); + + // Encode keys in bech32 format + const nsec = nostrTools.nip19.nsecEncode(privateKeyBytes); + const npub = nostrTools.nip19.npubEncode(publicKey); + + // Make sure we return the object with all properties + return { + privateKey, + publicKey, + nsec, + npub + }; + } catch (error) { + console.error('[MobileSigner] Error generating key pair:', error); + throw error; // Return the actual error for better debugging + } +} \ No newline at end of file diff --git a/lib/stores/ndk.ts b/lib/stores/ndk.ts index 6063492..0de1747 100644 --- a/lib/stores/ndk.ts +++ b/lib/stores/ndk.ts @@ -1,54 +1,116 @@ -// lib/stores/ndk.ts +// stores/ndk.ts +import '@/lib/crypto-polyfill'; // Import crypto polyfill first import { create } from 'zustand'; -import NDK, { NDKEvent, NDKPrivateKeySigner, NDKUser } from '@nostr-dev-kit/ndk'; +import NDK, { NDKFilter, NDKEvent as NDKEventBase } from '@nostr-dev-kit/ndk'; +import { NDKUser } from '@nostr-dev-kit/ndk-mobile'; +import { NDKEvent } from '@nostr-dev-kit/ndk-mobile'; import * as SecureStore from 'expo-secure-store'; -import { nip19 } from 'nostr-tools'; +import { NDKMobilePrivateKeySigner, generateKeyPair } from '@/lib/mobile-signer'; +import { setupCryptoPolyfill } from '@/lib/crypto-polyfill'; // Constants for SecureStore const PRIVATE_KEY_STORAGE_KEY = 'nostr_privkey'; // Default relays const DEFAULT_RELAYS = [ - 'ws://localhost:8080', // Add your local test relay - //'wss://relay.damus.io', - //'wss://relay.nostr.band', - //'wss://purplepag.es', - //'wss://nos.lol' + 'wss://powr.duckdns.org', // Your primary relay + 'wss://relay.damus.io', + 'wss://relay.nostr.band', + 'wss://nos.lol' ]; -// Helper function to convert Array/Uint8Array to hex string -function arrayToHex(array: number[] | Uint8Array): string { - return Array.from(array) - .map(b => b.toString(16).padStart(2, '0')) - .join(''); -} - type NDKStoreState = { ndk: NDK | null; currentUser: NDKUser | null; isLoading: boolean; isAuthenticated: boolean; - init: () => Promise; - login: (privateKey: string) => Promise; - logout: () => Promise; - getPublicKey: () => Promise; + error: Error | null; + relayStatus: Record; }; -export const useNDKStore = create((set, get) => ({ +type NDKStoreActions = { + init: () => Promise; + login: (privateKey?: string) => Promise; + logout: () => Promise; + generateKeys: () => { privateKey: string; publicKey: string; nsec: string; npub: string }; + publishEvent: (kind: number, content: string, tags: string[][]) => Promise; + fetchEventsByFilter: (filter: NDKFilter) => Promise; +}; + +export const useNDKStore = create((set, get) => ({ + // State properties ndk: null, currentUser: null, - isLoading: true, + isLoading: false, isAuthenticated: false, - + error: null, + relayStatus: {}, + + // Initialize NDK init: async () => { try { console.log('[NDK] Initializing...'); + console.log('NDK init crypto polyfill check:', { + cryptoDefined: typeof global.crypto !== 'undefined', + getRandomValuesDefined: typeof global.crypto?.getRandomValues !== 'undefined' + }); + + set({ isLoading: true, error: null }); + + // Initialize relay status tracking + const relayStatus: Record = {}; + DEFAULT_RELAYS.forEach(r => { + relayStatus[r] = 'connecting'; + }); + set({ relayStatus }); + // Initialize NDK with relays const ndk = new NDK({ explicitRelayUrls: DEFAULT_RELAYS }); + // Connect to relays await ndk.connect(); + + // Setup relay status updates + DEFAULT_RELAYS.forEach(url => { + const relay = ndk.pool.getRelay(url); + if (relay) { + relay.on('connect', () => { + set(state => ({ + relayStatus: { + ...state.relayStatus, + [url]: 'connected' + } + })); + }); + + relay.on('disconnect', () => { + set(state => ({ + relayStatus: { + ...state.relayStatus, + [url]: 'disconnected' + } + })); + }); + + // Set error status if not connected within timeout + setTimeout(() => { + set(state => { + if (state.relayStatus[url] === 'connecting') { + return { + relayStatus: { + ...state.relayStatus, + [url]: 'error' + } + }; + } + return state; + }); + }, 10000); + } + }); + set({ ndk }); // Check for saved private key @@ -57,8 +119,8 @@ export const useNDKStore = create((set, get) => ({ console.log('[NDK] Found saved private key, initializing signer'); try { - // Create signer with private key - const signer = new NDKPrivateKeySigner(privateKey); + // Create mobile-specific signer with private key + const signer = new NDKMobilePrivateKeySigner(privateKey); ndk.signer = signer; // Get user and profile @@ -78,61 +140,41 @@ export const useNDKStore = create((set, get) => ({ await SecureStore.deleteItemAsync(PRIVATE_KEY_STORAGE_KEY); } } + + set({ isLoading: false }); } catch (error) { console.error('[NDK] Initialization error:', error); - } finally { - set({ isLoading: false }); + set({ + error: error instanceof Error ? error : new Error('Failed to initialize NDK'), + isLoading: false + }); } }, -// lib/stores/ndk.ts - updated login method -login: async (privateKey: string) => { - set({ isLoading: true }); + login: async (privateKey?: string) => { + set({ isLoading: true, error: null }); try { const { ndk } = get(); if (!ndk) { - console.error('[NDK] NDK not initialized'); - return false; + throw new Error('NDK not initialized'); } - // Process the private key (handle nsec format) - let hexKey = privateKey; - - if (privateKey.startsWith('nsec1')) { - try { - const { type, data } = nip19.decode(privateKey); - if (type !== 'nsec') { - throw new Error('Invalid nsec key'); - } - - // Handle different data types - if (typeof data === 'string') { - hexKey = data; - } else if (Array.isArray(data)) { - // Convert array to hex string - hexKey = arrayToHex(data); - } else if (data instanceof Uint8Array) { - // Convert Uint8Array to hex string - hexKey = arrayToHex(data); - } else { - throw new Error('Unsupported key format'); - } - } catch (error) { - console.error('[NDK] Key decode error:', error); - throw new Error('Invalid private key format'); - } + // If no private key is provided, generate one + let userPrivateKey = privateKey; + if (!userPrivateKey) { + const { privateKey: generatedKey } = get().generateKeys(); + userPrivateKey = generatedKey; } - // Create signer with hex key - console.log('[NDK] Creating signer with key'); - const signer = new NDKPrivateKeySigner(hexKey); + // Create mobile-specific signer with private key + const signer = new NDKMobilePrivateKeySigner(userPrivateKey); ndk.signer = signer; // Get user const user = await ndk.signer.user(); if (!user) { - throw new Error('Failed to get user from signer'); + throw new Error('Could not get user from signer'); } // Fetch user profile @@ -149,19 +191,22 @@ login: async (privateKey: string) => { } // Save the private key securely - await SecureStore.setItemAsync(PRIVATE_KEY_STORAGE_KEY, hexKey); + await SecureStore.setItemAsync(PRIVATE_KEY_STORAGE_KEY, userPrivateKey); set({ currentUser: user, - isAuthenticated: true + isAuthenticated: true, + isLoading: false }); return true; } catch (error) { console.error('[NDK] Login error:', error); + set({ + error: error instanceof Error ? error : new Error('Failed to login'), + isLoading: false + }); return false; - } finally { - set({ isLoading: false }); } }, @@ -176,7 +221,7 @@ login: async (privateKey: string) => { ndk.signer = undefined; } - // Completely reset the user state + // Reset the user state set({ currentUser: null, isAuthenticated: false @@ -188,11 +233,98 @@ login: async (privateKey: string) => { } }, - getPublicKey: async () => { - const { currentUser } = get(); - if (currentUser) { - return currentUser.pubkey; + generateKeys: () => { + try { + return generateKeyPair(); + } catch (error) { + console.error('[NDK] Error generating keys:', error); + set({ error: error instanceof Error ? error : new Error('Failed to generate keys') }); + throw error; + } + }, + + // In your publishEvent function in ndk.ts: + publishEvent: async (kind: number, content: string, tags: string[][]) => { + try { + const { ndk, isAuthenticated, currentUser } = get(); + + if (!ndk) { + throw new Error('NDK not initialized'); + } + + if (!isAuthenticated || !currentUser) { + throw new Error('Not authenticated'); + } + + // Define custom functions we'll use to override crypto + const customRandomBytes = (length: number): Uint8Array => { + console.log('Using custom randomBytes in event signing'); + // Use type assertion to avoid TypeScript error + return (Crypto as any).getRandomBytes(length); + }; + + // Create event + const event = new NDKEvent(ndk); + event.kind = kind; + event.content = content; + event.tags = tags; + + // Direct monkey-patching approach + try { + // Try to find and override the randomBytes function + const nostrTools = require('nostr-tools'); + const nobleHashes = require('@noble/hashes/utils'); + + // Backup original functions + const originalNobleRandomBytes = nobleHashes.randomBytes; + + // Override with our implementation + (nobleHashes as any).randomBytes = customRandomBytes; + + // Sign event + console.log('Signing event with patched libraries...'); + await event.sign(); + + // Restore original functions + (nobleHashes as any).randomBytes = originalNobleRandomBytes; + + console.log('Event signed successfully'); + } catch (signError) { + console.error('Error signing event:', signError); + throw signError; + } + + // Publish the event + console.log('Publishing event...'); + await event.publish(); + + console.log('Event published successfully:', event.id); + return event; + } catch (error) { + console.error('Error publishing event:', error); + console.error('Error details:', error instanceof Error ? error.stack : 'Unknown error'); + set({ error: error instanceof Error ? error : new Error('Failed to publish event') }); + return null; + } + }, + + fetchEventsByFilter: async (filter: NDKFilter) => { + try { + const { ndk } = get(); + + if (!ndk) { + throw new Error('NDK not initialized'); + } + + // Fetch events + const events = await ndk.fetchEvents(filter); + + // Convert Set to Array + return Array.from(events); + } catch (error) { + console.error('Error fetching events:', error); + set({ error: error instanceof Error ? error : new Error('Failed to fetch events') }); + return []; } - return null; } })); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c3b8312..7db862f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,10 @@ "hasInstallScript": true, "dependencies": { "@expo/cli": "^0.22.16", + "@noble/hashes": "^1.7.1", "@noble/secp256k1": "^2.2.3", "@nostr-dev-kit/ndk": "^2.12.0", + "@nostr-dev-kit/ndk-mobile": "^0.4.1", "@radix-ui/react-alert-dialog": "^1.1.6", "@react-native-clipboard/clipboard": "^1.16.1", "@react-navigation/material-top-tabs": "^7.1.0", @@ -46,9 +48,11 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "expo": "^52.0.35", + "expo-crypto": "~14.0.2", "expo-file-system": "~18.0.10", "expo-linking": "~7.0.4", "expo-navigation-bar": "~4.0.8", + "expo-random": "^14.0.1", "expo-router": "~4.0.16", "expo-secure-store": "~14.0.1", "expo-splash-screen": "~0.29.20", @@ -64,6 +68,7 @@ "react-dom": "18.3.1", "react-native": "0.76.7", "react-native-gesture-handler": "~2.20.2", + "react-native-get-random-values": "~1.11.0", "react-native-pager-view": "6.5.1", "react-native-reanimated": "~3.16.1", "react-native-safe-area-context": "4.12.0", @@ -2289,12 +2294,127 @@ "node": ">=6.9.0" } }, + "node_modules/@bacons/text-decoder": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@bacons/text-decoder/-/text-decoder-0.0.0.tgz", + "integrity": "sha512-8KNbnXSHfhZRR1S1IQEdWQNa9HE/ylWRisDdkoCmHiaP53mksnPaxyqUSlwpJ3DyG1xEekRwFDEG+pbCbSsrkQ==", + "license": "MIT", + "peerDependencies": { + "react-native": "*" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "license": "MIT" }, + "node_modules/@cashu/cashu-ts": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@cashu/cashu-ts/-/cashu-ts-2.2.1.tgz", + "integrity": "sha512-/A8Lfkf7nexldcAcTbqrITXxwgiCYTTnrthB8DoipLVeDfyUXer48FJdUmXpRp87Aijn2BNklo8qA0yO0kHXaA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@cashu/crypto": "^0.3.4", + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", + "buffer": "^6.0.3" + } + }, + "node_modules/@cashu/cashu-ts/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/@cashu/crypto": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.3.4.tgz", + "integrity": "sha512-mfv1Pj4iL1PXzUj9NKIJbmncCLMqYfnEDqh/OPxAX0nNBt6BOnVJJLjLWFlQeYxlnEfWABSNkrqPje1t5zcyhA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", + "buffer": "^6.0.3" + } + }, + "node_modules/@cashu/crypto/node_modules/@scure/bip32": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", + "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/curves": "~1.8.1", + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@cashu/crypto/node_modules/@scure/bip39": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", + "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "~1.7.1", + "@scure/base": "~1.2.4" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@cashu/crypto/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/@egjs/hammerjs": { "version": "2.0.17", "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", @@ -3880,6 +4000,71 @@ "node": ">=16" } }, + "node_modules/@nostr-dev-kit/ndk-mobile": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-mobile/-/ndk-mobile-0.4.1.tgz", + "integrity": "sha512-ounoAkMrG5f2Vg+cvlW2x2DZJp4YupcJ4NZkOXumBspxbCbSjZ5B7ILSxo/2adgDwpGniXKQxnxoBVCqk2Lofw==", + "license": "MIT", + "dependencies": { + "@bacons/text-decoder": "^0.0.0", + "@nostr-dev-kit/ndk": "2.12.0", + "@nostr-dev-kit/ndk-wallet": "0.4.1", + "react-native-get-random-values": "~1.11.0", + "typescript-lru-cache": "^2.0.0", + "zustand": "^5.0.2" + }, + "peerDependencies": { + "expo": "*", + "expo-nip55": "*" + } + }, + "node_modules/@nostr-dev-kit/ndk-mobile/node_modules/zustand": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", + "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, + "node_modules/@nostr-dev-kit/ndk-wallet": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-wallet/-/ndk-wallet-0.4.1.tgz", + "integrity": "sha512-TSKjgxgKAo0PaXgKYUwqWeCV4Q0CioGBTYuWtPY80nkReJMss7TY0RKy+5DrEapwi2eN0OhZuwAyltzF8BDSiw==", + "license": "MIT", + "dependencies": { + "@nostr-dev-kit/ndk": "2.12.0", + "debug": "^4.3.4", + "light-bolt11-decoder": "^3.0.0", + "tseep": "^1.1.1", + "typescript": "^5.4.4", + "webln": "^0.3.2" + }, + "peerDependencies": { + "@cashu/cashu-ts": "*", + "@cashu/crypto": "*" + } + }, "node_modules/@npmcli/fs": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", @@ -8946,6 +9131,15 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/chrome": { + "version": "0.0.74", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.74.tgz", + "integrity": "sha512-hzosS5CkQcIKCgxcsV2AzbJ36KNxG/Db2YEN/erEu7Boprg+KpMDLBQqKFmSo+JkQMGqRcicUyqCowJpuT+C6A==", + "license": "MIT", + "dependencies": { + "@types/filesystem": "*" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -8981,6 +9175,21 @@ "license": "MIT", "peer": true }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", + "license": "MIT", + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -11982,6 +12191,18 @@ "react-native": "*" } }, + "node_modules/expo-crypto": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-14.0.2.tgz", + "integrity": "sha512-WRc9PBpJraJN29VD5Ef7nCecxJmZNyRKcGkNiDQC1nhY5agppzwhqh7zEzNFarE/GqDgSiaDHS8yd5EgFhP9AQ==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-file-system": { "version": "18.0.10", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.0.10.tgz", @@ -12121,6 +12342,31 @@ "react-native": "*" } }, + "node_modules/expo-nip55": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expo-nip55/-/expo-nip55-0.1.5.tgz", + "integrity": "sha512-TLeo7Kne7Yj138av1Zbvyrh/cG9jozXVeS42g8QJ2WdlnB329MzB6wmx7BkYS5x/MXTL+KvpL3AL8XVHHna51A==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-random": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/expo-random/-/expo-random-14.0.1.tgz", + "integrity": "sha512-gX2mtR9o+WelX21YizXUCD/y+a4ZL+RDthDmFkHxaYbdzjSYTn8u/igoje/l3WEO+/RYspmqUFa8w/ckNbt6Vg==", + "deprecated": "This package is now deprecated in favor of expo-crypto, which provides the same functionality. To migrate, replace all imports from expo-random with imports from expo-crypto.", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-router": { "version": "4.0.17", "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-4.0.17.tgz", @@ -12276,6 +12522,12 @@ "type": "^2.7.2" } }, + "node_modules/fast-base64-decode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz", + "integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -17663,6 +17915,18 @@ "react-native": "*" } }, + "node_modules/react-native-get-random-values": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/react-native-get-random-values/-/react-native-get-random-values-1.11.0.tgz", + "integrity": "sha512-4BTbDbRmS7iPdhYLRcz3PGFIpFJBwNZg9g42iwa2P6FOv9vZj/xJc678RZXnLNZzd0qd7Q3CCF6Yd+CU2eoXKQ==", + "license": "MIT", + "dependencies": { + "fast-base64-decode": "^1.0.0" + }, + "peerDependencies": { + "react-native": ">=0.56" + } + }, "node_modules/react-native-helmet-async": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/react-native-helmet-async/-/react-native-helmet-async-2.0.4.tgz", @@ -19804,7 +20068,6 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -20233,6 +20496,15 @@ "node": ">=12" } }, + "node_modules/webln": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/webln/-/webln-0.3.2.tgz", + "integrity": "sha512-YYT83aOCLup2AmqvJdKtdeBTaZpjC6/JDMe8o6x1kbTYWwiwrtWHyO//PAsPixF3jwFsAkj5DmiceB6w/QSe7Q==", + "license": "MIT", + "dependencies": { + "@types/chrome": "^0.0.74" + } + }, "node_modules/webpack": { "version": "5.98.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", diff --git a/package.json b/package.json index 26aa6d2..6ff0e85 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,10 @@ }, "dependencies": { "@expo/cli": "^0.22.16", + "@noble/hashes": "^1.7.1", "@noble/secp256k1": "^2.2.3", "@nostr-dev-kit/ndk": "^2.12.0", + "@nostr-dev-kit/ndk-mobile": "^0.4.1", "@radix-ui/react-alert-dialog": "^1.1.6", "@react-native-clipboard/clipboard": "^1.16.1", "@react-navigation/material-top-tabs": "^7.1.0", @@ -60,9 +62,11 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "expo": "^52.0.35", + "expo-crypto": "~14.0.2", "expo-file-system": "~18.0.10", "expo-linking": "~7.0.4", "expo-navigation-bar": "~4.0.8", + "expo-random": "^14.0.1", "expo-router": "~4.0.16", "expo-secure-store": "~14.0.1", "expo-splash-screen": "~0.29.20", @@ -78,6 +82,7 @@ "react-dom": "18.3.1", "react-native": "0.76.7", "react-native-gesture-handler": "~2.20.2", + "react-native-get-random-values": "~1.11.0", "react-native-pager-view": "6.5.1", "react-native-reanimated": "~3.16.1", "react-native-safe-area-context": "4.12.0", diff --git a/tsconfig.json b/tsconfig.json index ea33520..d4fc2d4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "expo/tsconfig.base", "compilerOptions": { + "moduleResolution": "bundler", "strict": true, "baseUrl": ".", "paths": { diff --git a/types/nostr.ts b/types/nostr.ts index 1308cac..29536c9 100644 --- a/types/nostr.ts +++ b/types/nostr.ts @@ -10,6 +10,7 @@ export interface NostrEvent { } export enum NostrEventKind { + TEXT = 1, EXERCISE = 33401, TEMPLATE = 33402, WORKOUT = 1301 // Updated from 33403 to 1301