diff --git a/app/privacy/page.tsx b/app/privacy/page.tsx index 888d45f3..9156112b 100644 --- a/app/privacy/page.tsx +++ b/app/privacy/page.tsx @@ -132,14 +132,28 @@ export default function PrivacyPage() {

- {/* No Deletion */} + {/* Data Deletion */}
-

Data Cannot Be Deleted

-

- Because data is stored on a blockchain, it cannot be deleted. There is no "delete my account" - button because no one has the power to remove data from the blockchain. Once you post something, - it exists permanently. This is a fundamental property of blockchain technology, not a choice we made. -

+

Data Deletion

+
+

+ You can delete your account and all associated data from Dash Platform. Unlike traditional + blockchains, Dash Platform documents can be deleted by their owners. When you delete your account: +

+ +
+

+ Important: While data is deleted from Dash Platform itself, third-party services such as + blockchain explorers, indexing platforms, or archive services may have copied your data before deletion. + We have no control over these external services, and they may retain historical copies of your content. +

+
+
{/* Third Parties */} diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 6de8c967..6864497e 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -26,11 +26,12 @@ import * as Switch from '@radix-ui/react-switch' import * as RadioGroup from '@radix-ui/react-radio-group' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' -import toast from 'react-hot-toast' import { KeyBackupSettings } from '@/components/settings/key-backup-settings' import { BlockedUsersSettings } from '@/components/settings/blocked-users' import { BlockListSettings } from '@/components/settings/block-list-settings' import { useDashPayContactsModal } from '@/hooks/use-dashpay-contacts-modal' +import { useDeleteAccountModal } from '@/hooks/use-delete-account-modal' +import { DeleteAccountModal } from '@/components/settings/delete-account-modal' type SettingsSection = 'main' | 'account' | 'contacts' | 'notifications' | 'privacy' | 'appearance' | 'about' const VALID_SECTIONS: SettingsSection[] = ['main', 'account', 'contacts', 'notifications', 'privacy', 'appearance', 'about'] @@ -117,11 +118,10 @@ function SettingsPage() { } } - // TODO: Implement account deletion + // Delete account modal + const { open: openDeleteAccountModal } = useDeleteAccountModal() const handleDeleteAccount = () => { - if (confirm('Are you sure you want to delete your account? This action cannot be undone.')) { - toast.error('Account deletion is not yet implemented') - } + openDeleteAccountModal() } const renderMainSettings = () => ( @@ -529,6 +529,7 @@ function SettingsPage() { return (
+
diff --git a/components/settings/delete-account-modal.tsx b/components/settings/delete-account-modal.tsx new file mode 100644 index 00000000..232a3872 --- /dev/null +++ b/components/settings/delete-account-modal.tsx @@ -0,0 +1,553 @@ +'use client' + +import { useState, useEffect } from 'react' +import { useRouter } from 'next/navigation' +import { Button } from '@/components/ui/button' +import { X, Loader2, AlertTriangle, Trash2, CheckCircle, XCircle } from 'lucide-react' +import { motion, AnimatePresence } from 'framer-motion' +import { useDeleteAccountModal } from '@/hooks/use-delete-account-modal' +import { useAuth } from '@/contexts/auth-context' +import { accountDeletionService, DeletionProgress } from '@/lib/services/account-deletion-service' + +/** + * Delete Account Modal + * + * Multi-step modal for permanent account deletion: + * 1. Warning - explain what will be deleted + * 2. Confirm - type "DELETE" to confirm + * 3. Progress - show deletion progress + * 4. Complete/Error - show result + */ +export function DeleteAccountModal() { + const router = useRouter() + const { user, logout } = useAuth() + const { + isOpen, + step, + progress, + result, + documentCounts, + isLoadingCounts, + setStep, + setProgress, + setResult, + setDocumentCounts, + setIsLoadingCounts, + reset + } = useDeleteAccountModal() + + const [confirmText, setConfirmText] = useState('') + const [understood, setUnderstood] = useState(false) + const [isDeleting, setIsDeleting] = useState(false) + + // Load document counts when modal opens + useEffect(() => { + const loadCounts = async () => { + if (!user?.identityId) return + + setIsLoadingCounts(true) + try { + const counts = await accountDeletionService.countUserDocuments(user.identityId) + setDocumentCounts(counts) + } catch (error) { + console.error('Failed to load document counts:', error) + // Continue anyway - counts are just informational + setDocumentCounts({}) + } finally { + setIsLoadingCounts(false) + } + } + + if (isOpen && step === 'warning' && user?.identityId && documentCounts === null && !isLoadingCounts) { + void loadCounts() + } + }, [isOpen, step, user?.identityId, documentCounts, isLoadingCounts, setIsLoadingCounts, setDocumentCounts]) + + const handleClose = () => { + if (isDeleting) return // Prevent closing during deletion + setConfirmText('') + setUnderstood(false) + reset() + } + + const handleStartDeletion = async () => { + if (!user?.identityId) return + + setIsDeleting(true) + setStep('progress') + + try { + const deletionResult = await accountDeletionService.deleteAccount( + user.identityId, + (progressUpdate: DeletionProgress) => { + setProgress(progressUpdate) + } + ) + + setResult(deletionResult) + + if (deletionResult.success) { + setStep('complete') + } else if (deletionResult.partialFailure) { + setStep('error') + } else { + setStep('error') + } + } catch (error) { + console.error('Deletion failed:', error) + setResult({ + success: false, + totalDeleted: 0, + totalFailed: 0, + errors: [{ + contractId: 'unknown', + documentType: 'unknown', + documentId: 'unknown', + error: error instanceof Error ? error.message : 'Unknown error' + }], + partialFailure: false + }) + setStep('error') + } finally { + setIsDeleting(false) + } + } + + const handleCompleteLogout = async () => { + try { + await logout() + } catch (error) { + console.error('Failed to logout after account deletion:', error) + // Navigate to login even if logout fails + router.push('/login') + } finally { + handleClose() + } + } + + const getTotalDocuments = () => { + if (!documentCounts) return 0 + return Object.values(documentCounts).reduce((sum, count) => sum + count, 0) + } + + const deletionSummary = accountDeletionService.getDeletionSummary() + + // Render warning step + const renderWarningStep = () => ( +
+ {/* Icon */} +
+
+ +
+
+ +

Delete Your Account

+ +

+ This will permanently delete all your data from Dash Platform. +

+ + {/* Document counts */} + {isLoadingCounts ? ( +
+ +

Counting your documents...

+
+ ) : documentCounts && getTotalDocuments() > 0 ? ( +
+

+ You have {getTotalDocuments()} documents to delete: +

+
+ {Object.entries(documentCounts).map(([type, count]) => ( +
+ {type.replace(/([A-Z])/g, ' $1').trim()} + {count} +
+ ))} +
+
+ ) : null} + + {/* What will be deleted */} +
+

+ + What will be deleted: +

+
+ {deletionSummary.map((category) => ( +
+

{category.category}

+
    + {category.items.map((item, i) => ( +
  • {item}
  • + ))} +
+
+ ))} +
+
+ + {/* Third-party warning */} +
+
+ +
+

+ Important Notice +

+

+ While your data will be deleted from Dash Platform, third-party indexers or explorers may retain historical copies. + The blockchain itself doesn't keep document history, but external services that have indexed your data may still have copies. +

+
+
+
+ + {/* Consent checkbox */} + + +
+ + +
+
+ ) + + // Render confirm step + const renderConfirmStep = () => ( +
+
+
+ +
+
+ +

Confirm Deletion

+ +

+ To confirm, type DELETE below +

+ +
+ setConfirmText(e.target.value.toUpperCase())} + placeholder="Type DELETE to confirm" + className="w-full px-4 py-3 bg-gray-50 dark:bg-gray-950 border border-gray-200 dark:border-gray-800 rounded-lg text-center text-lg font-mono tracking-widest focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent transition-colors" + autoComplete="off" + autoCorrect="off" + autoCapitalize="characters" + /> +
+ +
+ + +
+
+ ) + + // Render progress step + const renderProgressStep = () => { + const progressPercent = progress + ? Math.round((progress.processedDocuments / Math.max(progress.totalDocuments, 1)) * 100) + : 0 + + return ( +
+
+
+ +
+
+ +

Deleting Your Data

+ +

+ Please wait while we delete your documents... +

+ + {/* Progress bar */} +
+
+
+
+

+ {progress?.processedDocuments || 0} / {progress?.totalDocuments || 0} documents processed +

+
+ + {/* Current operation */} + {progress && ( +
+

+ {progress.phase === 'counting' && 'Counting documents...'} + {progress.phase === 'deleting' && ( + <>Deleting {progress.currentDocumentType} from {progress.currentContract}... + )} + {progress.phase === 'cleanup' && 'Cleaning up local data...'} +

+
+ )} + + {/* Stats */} + {progress && progress.processedDocuments > 0 && ( +
+
+

{progress.deletedDocuments}

+

Deleted

+
+ {progress.failedDocuments > 0 && ( +
+

{progress.failedDocuments}

+

Failed

+
+ )} +
+ )} + +

+ Do not close this window +

+
+ ) + } + + // Render complete step + const renderCompleteStep = () => ( +
+
+
+ +
+
+ +

Account Deleted

+ +

+ Your account data has been successfully deleted from Dash Platform. +

+ + {result && ( +
+

+ {result.totalDeleted} documents deleted +

+
+ )} + + +
+ ) + + // Render error step + const renderErrorStep = () => ( +
+
+
+ +
+
+ +

+ {result?.partialFailure ? 'Partial Deletion' : 'Deletion Failed'} +

+ +

+ {result?.partialFailure + ? 'Some documents could not be deleted. You may try again later.' + : 'We encountered an error while deleting your data.'} +

+ + {result && ( +
+ {result.totalDeleted > 0 && ( +
+

+ {result.totalDeleted} documents successfully deleted +

+
+ )} + + {result.totalFailed > 0 && ( +
+

+ {result.totalFailed} documents failed to delete +

+ {result.errors.length > 0 && result.errors.length <= 5 && ( +
    + {result.errors.map((err, i) => ( +
  • + {err.documentType}: {err.error} +
  • + ))} +
+ )} +
+ )} +
+ )} + +
+ {result?.partialFailure ? ( + <> + + + + ) : ( + <> + + + + )} +
+
+ ) + + const renderStep = () => { + switch (step) { + case 'warning': + return renderWarningStep() + case 'confirm': + return renderConfirmStep() + case 'progress': + return renderProgressStep() + case 'complete': + return renderCompleteStep() + case 'error': + return renderErrorStep() + default: + return renderWarningStep() + } + } + + return ( + + {isOpen && ( + <> + {/* Backdrop */} + + + {/* Modal */} + +
+ {/* Close button - hidden during deletion */} + {!isDeleting && step !== 'progress' && ( + + )} + + {renderStep()} +
+
+ + )} +
+ ) +} diff --git a/hooks/use-delete-account-modal.ts b/hooks/use-delete-account-modal.ts new file mode 100644 index 00000000..60a1732c --- /dev/null +++ b/hooks/use-delete-account-modal.ts @@ -0,0 +1,52 @@ +import { create } from 'zustand' +import { DeletionProgress, DeletionResult, DocumentCounts } from '@/lib/services/account-deletion-service' + +export type DeleteAccountStep = 'warning' | 'confirm' | 'progress' | 'complete' | 'error' + +interface DeleteAccountModalStore { + isOpen: boolean + step: DeleteAccountStep + progress: DeletionProgress | null + result: DeletionResult | null + documentCounts: DocumentCounts | null + isLoadingCounts: boolean + + // Actions + open: () => void + close: () => void + setStep: (step: DeleteAccountStep) => void + setProgress: (progress: DeletionProgress) => void + setResult: (result: DeletionResult) => void + setDocumentCounts: (counts: DocumentCounts) => void + setIsLoadingCounts: (loading: boolean) => void + reset: () => void +} + +const initialState = { + isOpen: false, + step: 'warning' as DeleteAccountStep, + progress: null, + result: null, + documentCounts: null, + isLoadingCounts: false +} + +export const useDeleteAccountModal = create((set) => ({ + ...initialState, + + open: () => set({ isOpen: true, step: 'warning' }), + + close: () => set({ isOpen: false }), + + setStep: (step) => set({ step }), + + setProgress: (progress) => set({ progress }), + + setResult: (result) => set({ result }), + + setDocumentCounts: (documentCounts) => set({ documentCounts }), + + setIsLoadingCounts: (isLoadingCounts) => set({ isLoadingCounts }), + + reset: () => set(initialState) +})) diff --git a/lib/services/account-deletion-service.ts b/lib/services/account-deletion-service.ts new file mode 100644 index 00000000..bca07b04 --- /dev/null +++ b/lib/services/account-deletion-service.ts @@ -0,0 +1,484 @@ +import { getEvoSdk } from './evo-sdk-service' +import { stateTransitionService } from './state-transition-service' +import { queryDocuments, identifierToBase58 } from './sdk-helpers' +import { + YAPPR_CONTRACT_ID, + YAPPR_PROFILE_CONTRACT_ID, + YAPPR_DM_CONTRACT_ID, + YAPPR_BLOCK_CONTRACT_ID, + ENCRYPTED_KEY_BACKUP_CONTRACT_ID, + HASHTAG_CONTRACT_ID +} from '../constants' + +/** + * Registry entry for a contract and its deletable document types + */ +interface DeletionRegistry { + contractId: string + documentTypes: string[] + serviceName: string +} + +/** + * Information about a document to be deleted + */ +interface DocumentInfo { + $id: string + $ownerId: string + documentType: string + contractId: string +} + +/** + * Progress update during deletion + */ +export interface DeletionProgress { + phase: 'counting' | 'deleting' | 'cleanup' | 'complete' | 'failed' + currentContract: string + currentDocumentType: string + totalDocuments: number + processedDocuments: number + deletedDocuments: number + failedDocuments: number + errors: Array<{ + contractId: string + documentType: string + documentId: string + error: string + }> +} + +/** + * Result of account deletion + */ +export interface DeletionResult { + success: boolean + totalDeleted: number + totalFailed: number + errors: DeletionProgress['errors'] + partialFailure: boolean +} + +/** + * Document counts by type for display + */ +export interface DocumentCounts { + [documentType: string]: number +} + +/** + * Account Deletion Service + * + * Handles comprehensive deletion of all user documents across all Yappr contracts. + * Uses a registry-based architecture for easy extensibility when new document types are added. + */ +class AccountDeletionService { + /** + * Registry of all contracts and document types that should be deleted. + * + * IMPORTANT: When adding new document types to Yappr, add them here to ensure + * they are deleted during account deletion. + */ + private readonly registry: DeletionRegistry[] = [ + // Main Yappr contract - social features + { + contractId: YAPPR_CONTRACT_ID, + documentTypes: [ + 'post', // User's posts + 'like', // User's likes + 'repost', // User's reposts + 'follow', // Users this user follows + 'bookmark', // User's bookmarks + 'list', // User's custom lists + 'listMember', // Members in user's lists + 'notification', // User's notifications + 'profile', // User's profile (old contract) + 'avatar', // User's avatar + ], + serviceName: 'Main Social' + }, + // Unified profile contract + { + contractId: YAPPR_PROFILE_CONTRACT_ID, + documentTypes: ['profile'], + serviceName: 'Profile' + }, + // Direct messages contract + { + contractId: YAPPR_DM_CONTRACT_ID, + documentTypes: ['directMessage'], + serviceName: 'Direct Messages' + }, + // Block/mute contract + { + contractId: YAPPR_BLOCK_CONTRACT_ID, + documentTypes: [ + 'block', // Blocked users + 'blockFilter', // Bloom filter for blocks + 'blockFollow', // Block follows + 'mute', // Muted users + ], + serviceName: 'Block System' + }, + // Encrypted key backup contract + { + contractId: ENCRYPTED_KEY_BACKUP_CONTRACT_ID, + documentTypes: ['encryptedKeyBackup'], + serviceName: 'Key Backup' + }, + // Hashtag tracking contract + { + contractId: HASHTAG_CONTRACT_ID, + documentTypes: ['postHashtag'], + serviceName: 'Hashtags' + } + ] + + /** + * Batch size for concurrent deletions within a document type + */ + private readonly BATCH_SIZE = 3 + + /** + * Delay between batches to avoid rate limiting (ms) + */ + private readonly BATCH_DELAY_MS = 500 + + /** + * Maximum documents to query per request (Platform limit) + */ + private readonly QUERY_LIMIT = 100 + + /** + * Count all user documents across all contracts + */ + async countUserDocuments(userId: string): Promise { + const counts: DocumentCounts = {} + + for (const entry of this.registry) { + for (const documentType of entry.documentTypes) { + try { + const documents = await this.queryUserDocuments( + entry.contractId, + documentType, + userId + ) + if (documents.length > 0) { + counts[documentType] = documents.length + } + } catch (error) { + // Log but continue - some document types may not exist for this user + console.warn(`Failed to count ${documentType}:`, error) + } + } + } + + return counts + } + + /** + * Query all documents owned by a user for a specific type + * Handles pagination for users with many documents + * Uses queryDocuments from sdk-helpers for proper error handling + */ + private async queryUserDocuments( + contractId: string, + documentType: string, + userId: string + ): Promise { + const sdk = await getEvoSdk() + const allDocuments: DocumentInfo[] = [] + let startAfter: string | undefined = undefined + + // Paginate through all documents + while (true) { + try { + const documents = await queryDocuments(sdk, { + dataContractId: contractId, + documentTypeName: documentType, + where: [['$ownerId', '==', userId]], + orderBy: [['$createdAt', 'asc']], + limit: this.QUERY_LIMIT, + startAfter + }) + + if (documents.length === 0) { + break + } + + // Add documents with metadata + for (const doc of documents) { + const docId = identifierToBase58(doc.$id) || (doc.$id as string) || (doc.id as string) + const ownerId = identifierToBase58(doc.$ownerId) || (doc.$ownerId as string) || (doc.ownerId as string) || userId + + allDocuments.push({ + $id: docId, + $ownerId: ownerId, + documentType, + contractId + }) + } + + // Check if we need to paginate + if (documents.length < this.QUERY_LIMIT) { + break + } + + // Use last document ID for pagination + const lastDoc = documents[documents.length - 1] + startAfter = identifierToBase58(lastDoc.$id) || (lastDoc.$id as string) || (lastDoc.id as string) + } catch (error) { + // Some document types may fail to query (e.g., not registered) + console.warn(`Query failed for ${documentType} in contract ${contractId}:`, error) + break + } + } + + return allDocuments + } + + /** + * Delete all user documents with progress callback + */ + async deleteAllUserDocuments( + userId: string, + onProgress: (progress: DeletionProgress) => void + ): Promise { + const progress: DeletionProgress = { + phase: 'counting', + currentContract: '', + currentDocumentType: '', + totalDocuments: 0, + processedDocuments: 0, + deletedDocuments: 0, + failedDocuments: 0, + errors: [] + } + + // First, collect all documents to delete + const allDocuments: DocumentInfo[] = [] + + for (const entry of this.registry) { + progress.currentContract = entry.serviceName + + for (const documentType of entry.documentTypes) { + progress.currentDocumentType = documentType + onProgress({ ...progress }) + + try { + const documents = await this.queryUserDocuments( + entry.contractId, + documentType, + userId + ) + allDocuments.push(...documents) + progress.totalDocuments = allDocuments.length + onProgress({ ...progress }) + } catch (error) { + console.warn(`Failed to query ${documentType}:`, error) + } + } + } + + // Now delete all documents + progress.phase = 'deleting' + onProgress({ ...progress }) + + // Process deletions in batches + for (let i = 0; i < allDocuments.length; i += this.BATCH_SIZE) { + const batch = allDocuments.slice(i, i + this.BATCH_SIZE) + + // Update progress with current document type being processed + if (batch.length > 0) { + progress.currentDocumentType = batch[0].documentType + progress.currentContract = this.getServiceName(batch[0].contractId) + } + onProgress({ ...progress }) + + // Process batch concurrently + const results = await Promise.allSettled( + batch.map(doc => this.deleteDocument(doc)) + ) + + // Update progress based on results + for (let j = 0; j < results.length; j++) { + progress.processedDocuments++ + const result = results[j] + + if (result.status === 'fulfilled' && result.value.success) { + progress.deletedDocuments++ + } else { + progress.failedDocuments++ + progress.errors.push({ + contractId: batch[j].contractId, + documentType: batch[j].documentType, + documentId: batch[j].$id, + error: result.status === 'rejected' + ? result.reason?.message || 'Unknown error' + : (result.value as { error?: string }).error || 'Delete failed' + }) + } + + onProgress({ ...progress }) + } + + // Add delay between batches to avoid rate limiting + if (i + this.BATCH_SIZE < allDocuments.length) { + await this.delay(this.BATCH_DELAY_MS) + } + } + + // Determine final status + const totalFailed = progress.failedDocuments + const totalDeleted = progress.deletedDocuments + + progress.phase = totalFailed === 0 ? 'complete' : 'failed' + onProgress({ ...progress }) + + return { + success: totalFailed === 0, + totalDeleted, + totalFailed, + errors: progress.errors, + partialFailure: totalFailed > 0 && totalDeleted > 0 + } + } + + /** + * Delete a single document + */ + private async deleteDocument(doc: DocumentInfo): Promise<{ success: boolean; error?: string }> { + try { + const result = await stateTransitionService.deleteDocument( + doc.contractId, + doc.documentType, + doc.$id, + doc.$ownerId + ) + return { success: result.success, error: result.error } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + } + } + } + + /** + * Clear all local storage related to the user session + */ + async clearLocalStorage(userId: string): Promise { + // Clear session storage items + if (typeof window !== 'undefined') { + localStorage.removeItem('yappr_session') + sessionStorage.removeItem('yappr_dpns_username') + sessionStorage.removeItem('yappr_skip_dpns') + sessionStorage.removeItem('yappr_backup_prompt_shown') + + // Clear private key from secure storage + const { clearPrivateKey } = await import('../secure-storage') + clearPrivateKey(userId) + + // Clear password-encrypted credentials for this user + const { removeStoredCredential } = await import('../password-encrypted-storage') + removeStoredCredential(userId) + + // Clear block cache + const { invalidateBlockCache } = await import('../caches/block-cache') + invalidateBlockCache(userId) + + // Clear DashPlatformClient identity + const { getDashPlatformClient } = await import('../dash-platform-client') + const dashClient = getDashPlatformClient() + dashClient.setIdentity('') + } + } + + /** + * Full account deletion orchestration + */ + async deleteAccount( + userId: string, + onProgress: (progress: DeletionProgress) => void + ): Promise { + // Delete all documents + const result = await this.deleteAllUserDocuments(userId, onProgress) + + // If deletion was successful or partially successful, clear local storage + if (result.totalDeleted > 0 || result.totalFailed === 0) { + const progress: DeletionProgress = { + phase: 'cleanup', + currentContract: '', + currentDocumentType: 'Local Storage', + totalDocuments: result.totalDeleted + result.totalFailed, + processedDocuments: result.totalDeleted + result.totalFailed, + deletedDocuments: result.totalDeleted, + failedDocuments: result.totalFailed, + errors: result.errors + } + onProgress(progress) + + await this.clearLocalStorage(userId) + } + + return result + } + + /** + * Get service name for a contract ID + */ + private getServiceName(contractId: string): string { + const entry = this.registry.find(e => e.contractId === contractId) + return entry?.serviceName || 'Unknown' + } + + /** + * Helper to delay execution + */ + private delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) + } + + /** + * Get all document types that will be deleted + * Useful for displaying to the user what will be affected + */ + getAffectedDocumentTypes(): string[] { + const types: string[] = [] + for (const entry of this.registry) { + types.push(...entry.documentTypes) + } + return Array.from(new Set(types)) // Remove duplicates + } + + /** + * Get human-readable descriptions of what will be deleted + */ + getDeletionSummary(): Array<{ category: string; items: string[] }> { + return [ + { + category: 'Social Content', + items: ['All your posts', 'All your replies', 'All your reposts/quotes'] + }, + { + category: 'Interactions', + items: ['All your likes', 'All your bookmarks', 'All users you follow'] + }, + { + category: 'Profile Data', + items: ['Your profile information', 'Your avatar', 'Your display name and bio'] + }, + { + category: 'Private Data', + items: ['All your direct messages', 'Your block and mute lists'] + }, + { + category: 'Other Data', + items: ['Your custom lists', 'Notification history', 'Encrypted key backups'] + } + ] + } +} + +// Export singleton instance +export const accountDeletionService = new AccountDeletionService()