diff --git a/.gitignore b/.gitignore index c246ae7..141246a 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ logs # Temporary files .tmp/ .temp/ +.pnpm-store/ # Solana specific .anchor/ @@ -80,4 +81,5 @@ yarn.lock #cursor .cursor/ -/commerce-kit/ \ No newline at end of file +#references +/references/ \ No newline at end of file diff --git a/apps/app/.env.example b/apps/app/.env.example new file mode 100644 index 0000000..769c464 --- /dev/null +++ b/apps/app/.env.example @@ -0,0 +1,10 @@ +# Solana RPC URL +# Set this to use a custom Solana RPC endpoint (e.g., Helius, QuickNode, or your own node) +# If not set, defaults to https://api.devnet.solana.com +# This variable is exposed to the client-side and will be available in production builds +# Example values: +# - Devnet: https://api.devnet.solana.com +# - Testnet: https://api.testnet.solana.com +# - Mainnet: https://api.mainnet-beta.solana.com +# - Custom RPC: https://your-rpc-endpoint.com +NEXT_PUBLIC_SOLANA_RPC_URL= diff --git a/apps/app/README.md b/apps/app/README.md index a55dfd0..f1b1ed3 100644 --- a/apps/app/README.md +++ b/apps/app/README.md @@ -63,7 +63,7 @@ src/ │ └─ sections/hero.tsx # Landing hero ├─ context/ │ ├─ ChainContextProvider.tsx # Cluster selection (devnet/testnet/mainnet) -│ ├─ RpcContextProvider.tsx # gill RPC + subscriptions +│ ├─ RpcContextProvider.tsx # @solana/kit RPC + subscriptions │ └─ SelectedWalletAccount* # Selected wallet state ├─ lib/ │ ├─ issuance/* # High-level create flows using @mosaic/sdk @@ -80,6 +80,10 @@ src/ - RPC/cluster: provided by `ChainContextProvider` and `RpcContextProvider` (Devnet/Testnet/Mainnet) - SDK: all blockchain operations use `@mosaic/sdk` +### Environment Variables + +- `NEXT_PUBLIC_SOLANA_RPC_URL`: Custom Solana RPC endpoint URL. If not set, defaults to `https://api.devnet.solana.com`. This variable is exposed to the client-side and available in production builds. See `.env.example` for more details. + ## Development ```bash @@ -99,4 +103,4 @@ pnpm start - Next.js 15, React 18, TailwindCSS - Wallet adapters (`@solana/wallet-adapter-*`) -- Mosaic SDK (`@mosaic/sdk`) and `gill` (`@solana/kit`) +- Mosaic SDK (`@mosaic/sdk`) and `@solana/kit` diff --git a/apps/app/next-env.d.ts b/apps/app/next-env.d.ts index 1b3be08..830fb59 100644 --- a/apps/app/next-env.d.ts +++ b/apps/app/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/app/package.json b/apps/app/package.json index e1b0961..a430a1f 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -4,52 +4,68 @@ "type": "module", "private": true, "scripts": { - "dev": "next dev", - "build": "next build", + "dev": "next dev --turbopack", + "build": "next build --turbopack", "start": "next start", "lint": "next lint", "type-check": "tsc --noEmit" }, "dependencies": { "@mosaic/sdk": "workspace:*", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-avatar": "^1.1.1", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-hover-card": "^1.1.15", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.3", - "@solana/accounts": "^2.3.0", - "@solana/react": "^2.3.0", - "@solana/signers": "^2.3.0", - "@solana/wallet-adapter-base": "^0.9.27", - "@solana/wallet-adapter-react": "^0.15.39", - "@solana/wallet-adapter-react-ui": "^0.9.39", - "@solana/wallet-adapter-wallets": "^0.19.37", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "@solana/accounts": "^5.0.0", + "@solana/connector": "0.1.4", + "@solana/react": "^5.0.0", + "@solana/kit": "^5.0.0", + "@solana-program/system": "^0.7.0", + "@solana-program/token-2022": "^0.5.0", + "@solana/sysvars": "^3.0.3", "@tanstack/react-query": "^5.83.0", - "@token-acl/abl-sdk": "^0.1.2", + "@token-acl/abl-sdk": "^0.2.0", "@wallet-standard/core": "^1.1.1", - "@wallet-standard/react": "^1.0.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "gill": "^0.10.2", - "gill-react": "^0.4.4", + "immer": "^10.1.1", "lucide-react": "^0.294.0", - "next": "15.4.8", + "motion": "^11.11.17", + "next": "15.5.7", "next-themes": "^0.2.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^19.2.1", + "react-dom": "^19.2.1", + "sonner": "^2.0.7", + "symbols-react": "^1.2.7", "tailwind-merge": "^2.6.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zustand": "^5.0.8" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.32.0", "@tailwindcss/postcss": "^4.1.17", "@types/node": "^20.10.0", - "@types/react": "^18.2.39", - "@types/react-dom": "^18.2.17", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", "@typescript-eslint/eslint-plugin": "^8.34.0", "@typescript-eslint/parser": "^8.34.0", "autoprefixer": "^10.4.21", "eslint": "^9.32.0", - "eslint-config-next": "15.4.8", + "eslint-config-next": "15.5.7", "eslint-plugin-import": "^2.32.0", "postcss": "^8.5.6", "tailwindcss": "^4.1.17", diff --git a/apps/app/src/app/dashboard/components/TokenCard.tsx b/apps/app/src/app/dashboard/components/TokenCard.tsx deleted file mode 100644 index 945e67b..0000000 --- a/apps/app/src/app/dashboard/components/TokenCard.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { useContext, useEffect, useState, useCallback } from 'react'; -import { Button } from '@/components/ui/button'; -import { Settings, Coins, Trash2, ExternalLink, RefreshCw } from 'lucide-react'; -import Link from 'next/link'; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { TokenDisplay } from '@/types/token'; -import { RpcContext } from '@/context/RpcContext'; -import { getTokenSupply } from '@/lib/utils'; -import { getTokenPatternsLabel } from '@/lib/token/tokenTypeUtils'; -import { type Address } from 'gill'; - -interface TokenCardProps { - token: TokenDisplay; - index: number; - onDelete: (address: string) => void; -} - -export function TokenCard({ token, index, onDelete }: TokenCardProps) { - const { rpc } = useContext(RpcContext); - const [currentSupply, setCurrentSupply] = useState(token.supply || '0'); - const [isLoadingSupply, setIsLoadingSupply] = useState(false); - - const fetchSupply = useCallback(async () => { - if (!token.address) return; - - setIsLoadingSupply(true); - try { - const supply = await getTokenSupply(rpc, token.address as Address); - setCurrentSupply(supply); - } catch { - // Silently handle errors and fall back to stored supply - setCurrentSupply(token.supply || '0'); - } finally { - setIsLoadingSupply(false); - } - }, [rpc, token.address, token.supply]); - - // Fetch supply on component mount - useEffect(() => { - fetchSupply(); - }, [fetchSupply]); - - const formatDate = (dateString?: string) => { - if (!dateString) return 'Unknown'; - return new Date(dateString).toLocaleDateString(); - }; - - return ( - - -
-
- -
- {token.name || `Token ${index + 1}`} - {token.symbol || 'TKN'} -
-
- - - - - - - - - Manage - - - {token.address && ( - - - - View on Solscan - - - )} - onDelete(token.address!)} className="text-red-600"> - - Delete from Storage - - - -
-
- -
-
- Type: -
- {getTokenPatternsLabel(token.detectedPatterns)} - {token.detectedPatterns && token.detectedPatterns.length > 1 && ( - - {token.detectedPatterns.length} patterns - - )} -
-
-
- Supply: -
- {isLoadingSupply ? 'Loading...' : currentSupply} - -
-
-
- Decimals: - {token.decimals || '6'} -
-
- Created: - {formatDate(token.createdAt)} -
-
- Status: - Active -
- {token.extensions && token.extensions.length > 0 && ( -
- Extensions: -
- {token.extensions.slice(0, 2).map((ext, idx) => ( - - {ext} - - ))} - {token.extensions.length > 2 && ( - - +{token.extensions.length - 2} more - - )} -
-
- )} -
-
- - - - - -
- ); -} diff --git a/apps/app/src/app/dashboard/create/arcade-token/ArcadeTokenAuthorityParams.tsx b/apps/app/src/app/dashboard/create/arcade-token/ArcadeTokenAuthorityParams.tsx deleted file mode 100644 index 7c46fef..0000000 --- a/apps/app/src/app/dashboard/create/arcade-token/ArcadeTokenAuthorityParams.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { useState } from 'react'; -import { Card, CardContent, CardHeader } from '@/components/ui/card'; -import { ChevronRight } from 'lucide-react'; -import { ArcadeTokenOptions } from '@/types/token'; - -interface ArcadeTokenAuthorityParamsProps { - options: ArcadeTokenOptions; - onInputChange: (field: string, value: string) => void; -} - -export function ArcadeTokenAuthorityParams({ options, onInputChange }: ArcadeTokenAuthorityParamsProps) { - const [showOptionalParams, setShowOptionalParams] = useState(false); - - return ( - - - - - {showOptionalParams && ( - -
- - onInputChange('mintAuthority', e.target.value)} - /> -
-
- - onInputChange('metadataAuthority', e.target.value)} - /> -
-
- - onInputChange('pausableAuthority', e.target.value)} - /> -
-
- - onInputChange('permanentDelegateAuthority', e.target.value)} - /> -
-
- )} -
- ); -} diff --git a/apps/app/src/app/dashboard/create/arcade-token/ArcadeTokenBasicParams.tsx b/apps/app/src/app/dashboard/create/arcade-token/ArcadeTokenBasicParams.tsx deleted file mode 100644 index 45dff78..0000000 --- a/apps/app/src/app/dashboard/create/arcade-token/ArcadeTokenBasicParams.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { EnableSrfc37Toggle } from '@/components/EnableSrfc37Toggle'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { ArcadeTokenOptions } from '@/types/token'; - -interface ArcadeTokenBasicParamsProps { - options: ArcadeTokenOptions; - onInputChange: (field: string, value: string) => void; -} - -export function ArcadeTokenBasicParams({ options, onInputChange }: ArcadeTokenBasicParamsProps) { - return ( - - - Basic Parameters - Configure the fundamental properties of your arcade token - - -
-
- - onInputChange('name', e.target.value)} - required - /> -
-
- - onInputChange('symbol', e.target.value)} - required - /> -
-
-
-
- - onInputChange('decimals', e.target.value)} - min="0" - max="9" - required - /> -
-
- - onInputChange('uri', e.target.value)} - /> -
-
- - onInputChange('enableSrfc37', value)} - /> -
-
- ); -} diff --git a/apps/app/src/app/dashboard/create/arcade-token/ArcadeTokenCreateForm.tsx b/apps/app/src/app/dashboard/create/arcade-token/ArcadeTokenCreateForm.tsx deleted file mode 100644 index f27b414..0000000 --- a/apps/app/src/app/dashboard/create/arcade-token/ArcadeTokenCreateForm.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { ArcadeTokenOptions, ArcadeTokenCreationResult } from '@/types/token'; -import { ArcadeTokenBasicParams } from './ArcadeTokenBasicParams'; -import { ArcadeTokenAuthorityParams } from './ArcadeTokenAuthorityParams'; -import { ArcadeTokenCreationResultDisplay } from '@/app/dashboard/create/arcade-token/ArcadeTokenCreationResult'; -import { createArcadeToken } from '@/lib/issuance/arcadeToken'; -import { TransactionSendingSigner } from '@solana/signers'; -import { TokenStorage, createTokenDisplayFromResult } from '@/lib/token/tokenStorage'; - -interface ArcadeTokenCreateFormProps { - transactionSendingSigner: TransactionSendingSigner; -} - -export function ArcadeTokenCreateForm({ transactionSendingSigner }: ArcadeTokenCreateFormProps) { - const [arcadeTokenOptions, setArcadeTokenOptions] = useState({ - name: '', - symbol: '', - decimals: '6', - uri: '', - enableSrfc37: false, - mintAuthority: '', - metadataAuthority: '', - pausableAuthority: '', - permanentDelegateAuthority: '', - }); - const [isCreating, setIsCreating] = useState(false); - const [result, setResult] = useState(null); - - const handleInputChange = (field: string, value: string) => { - setArcadeTokenOptions(prev => ({ ...prev, [field]: value })); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - setIsCreating(true); - setResult(null); - - try { - const result = await createArcadeToken(arcadeTokenOptions, transactionSendingSigner); - - if (result.success && result.mintAddress) { - // Create token display object - const tokenDisplay = createTokenDisplayFromResult(result, 'arcade-token', arcadeTokenOptions); - - // Save to local storage - TokenStorage.saveToken(tokenDisplay); - - const addrValue: unknown = ( - transactionSendingSigner as { - address?: unknown; - } - ).address; - const defaultAuthority = - typeof addrValue === 'string' - ? addrValue - : typeof addrValue === 'object' && addrValue !== null && 'toString' in addrValue - ? String((addrValue as { toString: () => string }).toString()) - : ''; - - setResult({ - success: true, - mintAddress: result.mintAddress, - transactionSignature: result.transactionSignature, - details: { - ...arcadeTokenOptions, - decimals: parseInt(arcadeTokenOptions.decimals), - enableSrfc37: arcadeTokenOptions.enableSrfc37 || false, - mintAuthority: arcadeTokenOptions.mintAuthority || defaultAuthority, - metadataAuthority: arcadeTokenOptions.metadataAuthority || defaultAuthority, - pausableAuthority: arcadeTokenOptions.pausableAuthority || defaultAuthority, - permanentDelegateAuthority: arcadeTokenOptions.permanentDelegateAuthority || defaultAuthority, - extensions: ['Metadata', 'Pausable', 'Default Account State (Allowlist)', 'Permanent Delegate'], - }, - }); - } else { - setResult({ - success: false, - error: result.error || 'Unknown error occurred', - }); - } - } catch (error) { - setResult({ - success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', - }); - } finally { - setIsCreating(false); - } - }; - - const handleReset = () => { - setArcadeTokenOptions({ - name: '', - symbol: '', - decimals: '6', - uri: '', - enableSrfc37: false, - mintAuthority: '', - metadataAuthority: '', - pausableAuthority: '', - permanentDelegateAuthority: '', - }); - setResult(null); - }; - - return ( - <> - {result ? ( - <> - -
- -
- - ) : ( -
- - - - -
- - -
- - )} - - ); -} diff --git a/apps/app/src/app/dashboard/create/arcade-token/page.tsx b/apps/app/src/app/dashboard/create/arcade-token/page.tsx deleted file mode 100644 index 51e603d..0000000 --- a/apps/app/src/app/dashboard/create/arcade-token/page.tsx +++ /dev/null @@ -1,121 +0,0 @@ -'use client'; - -import { useContext } from 'react'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import Link from 'next/link'; -import { CreateTemplateSidebar } from '@/components/CreateTemplateSidebar'; -import { SelectedWalletAccountContext } from '@/context/SelectedWalletAccountContext'; -import { ChainContext } from '@/context/ChainContext'; -import { useWalletAccountTransactionSendingSigner } from '@solana/react'; -import { UiWalletAccount } from '@wallet-standard/react'; -import { ArcadeTokenCreateForm } from '@/app/dashboard/create/arcade-token/ArcadeTokenCreateForm'; - -// Component that only renders when wallet is available -function ArcadeTokenCreateWithWallet({ - selectedWalletAccount, - currentChain, -}: { - selectedWalletAccount: UiWalletAccount; - currentChain: string; -}) { - // Now we can safely call the hook because we know we have valid inputs - const transactionSendingSigner = useWalletAccountTransactionSendingSigner( - selectedWalletAccount, - currentChain as `solana:${string}`, - ); - - return ( -
-
-
- - - -
-

Create Arcade Token

-

Configure your arcade token parameters

-
-
- -
- -
- -
-
-
-
- ); -} - -// Simple wrapper component that shows a message when wallet is not connected -function ArcadeTokenCreatePage() { - const [selectedWalletAccount] = useContext(SelectedWalletAccountContext); - const { chain: currentChain } = useContext(ChainContext); - - // If wallet is connected and chain is available, render the full component - if (selectedWalletAccount && currentChain) { - return ( - - ); - } - - // Otherwise, show a message to connect wallet - return ( -
-
-
- - - -
-

Create Arcade Token

-

Configure your arcade token parameters

-
-
- - - - Wallet Required - Please connect your wallet to create an arcade token - - -

- To create an arcade token, you need to connect a wallet first. Please use the wallet - connection button in the top navigation. -

-
-
-
-
- ); -} - -export default ArcadeTokenCreatePage; diff --git a/apps/app/src/app/dashboard/create/page.tsx b/apps/app/src/app/dashboard/create/page.tsx deleted file mode 100644 index c21bffd..0000000 --- a/apps/app/src/app/dashboard/create/page.tsx +++ /dev/null @@ -1,85 +0,0 @@ -'use client'; - -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { DollarSign, Gamepad2, CandlestickChart, Upload } from 'lucide-react'; -import Link from 'next/link'; - -export default function CreatePage() { - return ( -
-
-
-

Create New Token

-

Choose the type of token you want to create

-
- -
- - - -
- - Stablecoin -
-
- - - Create a regulatory-compliant stablecoin with transfer restrictions and metadata - management. - - -
- - - - - -
- - Arcade Token -
-
- - - Deploy a gaming or utility token with custom extensions and features. - - -
- - - - - -
- - Tokenized Security -
-
- - - Create a compliant security token with scaled UI amounts and core controls. - - -
- - - - - -
- - Import Existing Token -
-
- - - Import an existing token to manage it through the Mosaic platform. - - -
- -
-
-
- ); -} diff --git a/apps/app/src/app/dashboard/create/stablecoin/StablecoinAuthorityParams.tsx b/apps/app/src/app/dashboard/create/stablecoin/StablecoinAuthorityParams.tsx deleted file mode 100644 index 5e79aa3..0000000 --- a/apps/app/src/app/dashboard/create/stablecoin/StablecoinAuthorityParams.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { useState } from 'react'; -import { Card, CardContent, CardHeader } from '@/components/ui/card'; -import { ChevronRight } from 'lucide-react'; -import { StablecoinOptions } from '@/types/token'; - -interface StablecoinAuthorityParamsProps { - options: StablecoinOptions; - onInputChange: (field: string, value: string) => void; -} - -export function StablecoinAuthorityParams({ options, onInputChange }: StablecoinAuthorityParamsProps) { - const [showOptionalParams, setShowOptionalParams] = useState(false); - - return ( - - - - - {showOptionalParams && ( - -
- - onInputChange('mintAuthority', e.target.value)} - /> -
-
- - onInputChange('metadataAuthority', e.target.value)} - /> -
-
- - onInputChange('pausableAuthority', e.target.value)} - /> -
-
- - onInputChange('confidentialBalancesAuthority', e.target.value)} - /> -
-
- - onInputChange('permanentDelegateAuthority', e.target.value)} - /> -
-
- )} -
- ); -} diff --git a/apps/app/src/app/dashboard/create/stablecoin/StablecoinBasicParams.tsx b/apps/app/src/app/dashboard/create/stablecoin/StablecoinBasicParams.tsx deleted file mode 100644 index 173b0af..0000000 --- a/apps/app/src/app/dashboard/create/stablecoin/StablecoinBasicParams.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { EnableSrfc37Toggle } from '@/components/EnableSrfc37Toggle'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { StablecoinOptions } from '@/types/token'; - -interface StablecoinBasicParamsProps { - options: StablecoinOptions; - onInputChange: (field: string, value: string) => void; -} - -export function StablecoinBasicParams({ options, onInputChange }: StablecoinBasicParamsProps) { - return ( - - - Basic Parameters - Configure the fundamental properties of your stablecoin - - -
-
- - onInputChange('name', e.target.value)} - required - /> -
-
- - onInputChange('symbol', e.target.value)} - required - /> -
-
-
-
- - onInputChange('decimals', e.target.value)} - min="0" - max="9" - required - /> -
-
- - onInputChange('uri', e.target.value)} - /> -
-
- - onInputChange('enableSrfc37', value)} - /> -
-
- ); -} diff --git a/apps/app/src/app/dashboard/create/stablecoin/StablecoinCreateForm.tsx b/apps/app/src/app/dashboard/create/stablecoin/StablecoinCreateForm.tsx deleted file mode 100644 index 257e49f..0000000 --- a/apps/app/src/app/dashboard/create/stablecoin/StablecoinCreateForm.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { StablecoinOptions, StablecoinCreationResult } from '@/types/token'; -import { StablecoinBasicParams } from './StablecoinBasicParams'; -import { StablecoinAuthorityParams } from './StablecoinAuthorityParams'; -import { StablecoinCreationResultDisplay } from '@/app/dashboard/create/stablecoin/StablecoinCreationResult'; -import { createStablecoin } from '@/lib/issuance/stablecoin'; -import { TransactionSendingSigner } from '@solana/signers'; -import { TokenStorage, createTokenDisplayFromResult } from '@/lib/token/tokenStorage'; - -interface StablecoinCreateFormProps { - transactionSendingSigner: TransactionSendingSigner; -} - -export function StablecoinCreateForm({ transactionSendingSigner }: StablecoinCreateFormProps) { - const [stablecoinOptions, setStablecoinOptions] = useState({ - name: '', - symbol: '', - decimals: '6', - uri: '', - enableSrfc37: false, - aclMode: 'blocklist', - mintAuthority: '', - metadataAuthority: '', - pausableAuthority: '', - confidentialBalancesAuthority: '', - permanentDelegateAuthority: '', - }); - const [isCreating, setIsCreating] = useState(false); - const [result, setResult] = useState(null); - - const handleInputChange = (field: string, value: string) => { - setStablecoinOptions(prev => ({ ...prev, [field]: value })); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - setIsCreating(true); - setResult(null); - - try { - const result = await createStablecoin(stablecoinOptions, transactionSendingSigner); - - if (result.success && result.mintAddress) { - const addrValue: unknown = ( - transactionSendingSigner as { - address?: unknown; - } - ).address; - const defaultAuthority = - typeof addrValue === 'string' - ? addrValue - : typeof addrValue === 'object' && addrValue !== null && 'toString' in addrValue - ? String((addrValue as { toString: () => string }).toString()) - : ''; - - const derivedMintAuthority = stablecoinOptions.mintAuthority || defaultAuthority; - const derivedMetadataAuthority = stablecoinOptions.metadataAuthority || derivedMintAuthority; - const derivedPausableAuthority = stablecoinOptions.pausableAuthority || derivedMintAuthority; - const derivedConfidentialBalancesAuthority = - stablecoinOptions.confidentialBalancesAuthority || derivedMintAuthority; - const derivedPermanentDelegateAuthority = - stablecoinOptions.permanentDelegateAuthority || derivedMintAuthority; - // Create token display object - const tokenDisplay = createTokenDisplayFromResult(result, 'stablecoin', stablecoinOptions); - - // Save to local storage - TokenStorage.saveToken(tokenDisplay); - - setResult({ - success: true, - mintAddress: result.mintAddress, - transactionSignature: result.transactionSignature, - details: { - ...stablecoinOptions, - decimals: parseInt(stablecoinOptions.decimals), - aclMode: stablecoinOptions.aclMode, - mintAuthority: derivedMintAuthority, - metadataAuthority: derivedMetadataAuthority, - pausableAuthority: derivedPausableAuthority, - confidentialBalancesAuthority: derivedConfidentialBalancesAuthority, - permanentDelegateAuthority: derivedPermanentDelegateAuthority, - extensions: [ - 'Metadata', - 'Pausable', - `Default Account State (${stablecoinOptions.aclMode === 'allowlist' ? 'Allowlist' : 'Blocklist'})`, - 'Confidential Balances', - 'Permanent Delegate', - ], - }, - }); - } else { - setResult({ - success: false, - error: result.error || 'Unknown error occurred', - }); - } - } catch (error) { - setResult({ - success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', - }); - } finally { - setIsCreating(false); - } - }; - - const handleReset = () => { - setStablecoinOptions({ - name: '', - symbol: '', - decimals: '6', - uri: '', - enableSrfc37: false, - aclMode: 'blocklist', - mintAuthority: '', - metadataAuthority: '', - pausableAuthority: '', - confidentialBalancesAuthority: '', - permanentDelegateAuthority: '', - }); - setResult(null); - }; - - return ( - <> - {result ? ( - <> - -
- -
- - ) : ( -
- - -
-
-

Transfer Restrictions

-

- Choose whether to use an allowlist (closed-loop) or a blocklist. -

-
-
- - -
-
- - - -
- - -
- - )} - - ); -} diff --git a/apps/app/src/app/dashboard/create/stablecoin/page.tsx b/apps/app/src/app/dashboard/create/stablecoin/page.tsx deleted file mode 100644 index d44d59b..0000000 --- a/apps/app/src/app/dashboard/create/stablecoin/page.tsx +++ /dev/null @@ -1,122 +0,0 @@ -'use client'; - -import { useContext } from 'react'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import Link from 'next/link'; -import { CreateTemplateSidebar } from '@/components/CreateTemplateSidebar'; -import { SelectedWalletAccountContext } from '@/context/SelectedWalletAccountContext'; -import { ChainContext } from '@/context/ChainContext'; -import { useWalletAccountTransactionSendingSigner } from '@solana/react'; -import { UiWalletAccount } from '@wallet-standard/react'; -import { StablecoinCreateForm } from '@/app/dashboard/create/stablecoin/StablecoinCreateForm'; - -// Component that only renders when wallet is available -function StablecoinCreateWithWallet({ - selectedWalletAccount, - currentChain, -}: { - selectedWalletAccount: UiWalletAccount; - currentChain: string; -}) { - // Now we can safely call the hook because we know we have valid inputs - const transactionSendingSigner = useWalletAccountTransactionSendingSigner( - selectedWalletAccount, - currentChain as `solana:${string}`, - ); - - return ( -
-
-
- - - -
-

Create Stablecoin

-

Configure your stablecoin parameters

-
-
- -
- -
- -
-
-
-
- ); -} - -// Simple wrapper component that shows a message when wallet is not connected -function StablecoinCreatePage() { - const [selectedWalletAccount] = useContext(SelectedWalletAccountContext); - const { chain: currentChain } = useContext(ChainContext); - - // If wallet is connected and chain is available, render the full component - if (selectedWalletAccount && currentChain) { - return ; - } - - // Otherwise, show a message to connect wallet - return ( -
-
-
- - - -
-

Create Stablecoin

-

Configure your stablecoin parameters

-
-
- - - - Wallet Required - Please connect your wallet to create a stablecoin - - -

- To create a stablecoin, you need to connect a wallet first. Please use the wallet connection - button in the top navigation. -

-
-
-
-
- ); -} - -export default StablecoinCreatePage; diff --git a/apps/app/src/app/dashboard/create/tokenized-security/TokenizedSecurityAuthorityParams.tsx b/apps/app/src/app/dashboard/create/tokenized-security/TokenizedSecurityAuthorityParams.tsx deleted file mode 100644 index e8e0940..0000000 --- a/apps/app/src/app/dashboard/create/tokenized-security/TokenizedSecurityAuthorityParams.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { TokenizedSecurityOptions } from '@/types/token'; - -export function TokenizedSecurityAuthorityParams({ - options, - onInputChange, -}: { - options: TokenizedSecurityOptions; - onInputChange: (field: keyof TokenizedSecurityOptions, value: string) => void; -}) { - return ( -
-
-
- - onInputChange('mintAuthority', e.target.value)} - /> -
- -
-
- - onInputChange('metadataAuthority', e.target.value)} - /> -
-
- - onInputChange('pausableAuthority', e.target.value)} - /> -
-
- - onInputChange('confidentialBalancesAuthority', e.target.value)} - /> -
-
- - onInputChange('permanentDelegateAuthority', e.target.value)} - /> -
-
- - onInputChange('scaledUiAmountAuthority', e.target.value)} - /> -
-
-
-
- ); -} diff --git a/apps/app/src/app/dashboard/create/tokenized-security/TokenizedSecurityBasicParams.tsx b/apps/app/src/app/dashboard/create/tokenized-security/TokenizedSecurityBasicParams.tsx deleted file mode 100644 index a2243b8..0000000 --- a/apps/app/src/app/dashboard/create/tokenized-security/TokenizedSecurityBasicParams.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { EnableSrfc37Toggle } from '@/components/EnableSrfc37Toggle'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { TokenizedSecurityOptions } from '@/types/token'; - -export function TokenizedSecurityBasicParams({ - options, - onInputChange, -}: { - options: TokenizedSecurityOptions; - onInputChange: (field: keyof TokenizedSecurityOptions, value: string) => void; -}) { - return ( - - - Basic Parameters - Configure the fundamental properties of your tokenized security - - -
-
- - onInputChange('name', e.target.value)} - required - /> -
-
- - onInputChange('symbol', e.target.value)} - required - /> -
-
-
-
- - onInputChange('decimals', e.target.value)} - min={0} - max={9} - required - /> -
-
- - onInputChange('uri', e.target.value)} - /> -
-
- - onInputChange('enableSrfc37', value)} - /> -
-
- ); -} diff --git a/apps/app/src/app/dashboard/create/tokenized-security/TokenizedSecurityCreateForm.tsx b/apps/app/src/app/dashboard/create/tokenized-security/TokenizedSecurityCreateForm.tsx deleted file mode 100644 index 2b9be65..0000000 --- a/apps/app/src/app/dashboard/create/tokenized-security/TokenizedSecurityCreateForm.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { TokenizedSecurityCreationResultDisplay } from './TokenizedSecurityCreationResult'; -import { ChevronRight } from 'lucide-react'; -import { TokenizedSecurityOptions, TokenizedSecurityCreationResult } from '@/types/token'; -import { TokenizedSecurityBasicParams } from './TokenizedSecurityBasicParams'; -import { TokenizedSecurityAuthorityParams } from './TokenizedSecurityAuthorityParams'; -import { createTokenizedSecurity } from '@/lib/issuance/tokenizedSecurity'; -import { TransactionSendingSigner } from '@solana/signers'; -import { TokenStorage, createTokenDisplayFromResult } from '@/lib/token/tokenStorage'; - -export function TokenizedSecurityCreateForm({ - transactionSendingSigner, -}: { - transactionSendingSigner: TransactionSendingSigner; -}) { - const [options, setOptions] = useState({ - name: '', - symbol: '', - decimals: '6', - uri: '', - aclMode: 'blocklist', - enableSrfc37: false, - mintAuthority: '', - metadataAuthority: '', - pausableAuthority: '', - confidentialBalancesAuthority: '', - permanentDelegateAuthority: '', - scaledUiAmountAuthority: '', - multiplier: '1', - }); - const [isCreating, setIsCreating] = useState(false); - const [creationResult, setCreationResult] = useState(null); - const [showOptional, setShowOptional] = useState(false); - - const handleInputChange = (field: keyof TokenizedSecurityOptions, value: string) => { - setOptions(prev => ({ ...prev, [field]: value })); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsCreating(true); - setCreationResult(null); - try { - const result = await createTokenizedSecurity(options, transactionSendingSigner); - if (result.success && result.mintAddress) { - const tokenDisplay = createTokenDisplayFromResult(result, 'tokenized-security', options); - TokenStorage.saveToken(tokenDisplay); - } - setCreationResult(result); - } catch (error) { - setCreationResult({ - success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', - }); - } finally { - setIsCreating(false); - } - }; - - const handleReset = () => { - setOptions({ - name: '', - symbol: '', - decimals: '6', - uri: '', - aclMode: 'blocklist', - enableSrfc37: false, - mintAuthority: '', - metadataAuthority: '', - pausableAuthority: '', - confidentialBalancesAuthority: '', - permanentDelegateAuthority: '', - scaledUiAmountAuthority: '', - multiplier: '1', - }); - setCreationResult(null); - }; - - if (creationResult) { - return ( -
- - -
- ); - } - - return ( -
- - -
-
- -
- {showOptional && ( -
-
-
- - handleInputChange('multiplier', e.target.value)} - /> -
-
- - -
-
- - -
- )} -
- -
- - -
- - ); -} diff --git a/apps/app/src/app/dashboard/create/tokenized-security/page.tsx b/apps/app/src/app/dashboard/create/tokenized-security/page.tsx deleted file mode 100644 index e4f51ae..0000000 --- a/apps/app/src/app/dashboard/create/tokenized-security/page.tsx +++ /dev/null @@ -1,115 +0,0 @@ -'use client'; - -import { useContext } from 'react'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import Link from 'next/link'; -import { CreateTemplateSidebar } from '@/components/CreateTemplateSidebar'; -import { SelectedWalletAccountContext } from '@/context/SelectedWalletAccountContext'; -import { ChainContext } from '@/context/ChainContext'; -import { useWalletAccountTransactionSendingSigner } from '@solana/react'; -import { UiWalletAccount } from '@wallet-standard/react'; -import { TokenizedSecurityCreateForm } from './TokenizedSecurityCreateForm'; - -function TokenizedSecurityCreateWithWallet({ - selectedWalletAccount, - currentChain, -}: { - selectedWalletAccount: UiWalletAccount; - currentChain: string; -}) { - const transactionSendingSigner = useWalletAccountTransactionSendingSigner( - selectedWalletAccount, - currentChain as `solana:${string}`, - ); - - return ( -
-
-
- - - -
-

Create Tokenized Security

-

Configure your tokenized security parameters

-
-
- -
- -
- -
-
-
-
- ); -} - -export default function TokenizedSecurityCreatePage() { - const [selectedWalletAccount] = useContext(SelectedWalletAccountContext); - const { chain: currentChain } = useContext(ChainContext); - - if (selectedWalletAccount && currentChain) { - return ( - - ); - } - - return ( -
-
-
- - - -
-

Create Tokenized Security

-

Configure your tokenized security parameters

-
-
- - - - Wallet Required - Please connect your wallet to create a tokenized security - - -

- To create a tokenized security, you need to connect a wallet first. Please use the wallet - connection button in the top navigation. -

-
-
-
-
- ); -} diff --git a/apps/app/src/app/dashboard/manage/[address]/components/ActionResultModal.tsx b/apps/app/src/app/dashboard/manage/[address]/components/ActionResultModal.tsx deleted file mode 100644 index e2b442d..0000000 --- a/apps/app/src/app/dashboard/manage/[address]/components/ActionResultModal.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { Button } from '@/components/ui/button'; -import Link from 'next/link'; - -interface ActionResultModalProps { - isOpen: boolean; - onClose: () => void; - error: string; - transactionSignature: string; - actionInProgress: boolean; -} - -export function ActionResultModal({ - isOpen, - onClose, - error, - transactionSignature, - actionInProgress, -}: ActionResultModalProps) { - if (!isOpen) return null; - - return ( -
-
- {/* Header */} -
-

- {actionInProgress ? ( - - - - - - ) : error ? ( - - - - - - ) : ( - - - - - - )} - {actionInProgress ? 'Action in progress...' : error ? 'Error' : 'Success'} -

-
- - {/* Body */} -
- {/* Action in progress message */} - {actionInProgress && ( -
-

- Action in progress. Please sign and submit the transaction to complete the action. -

-
- )} - - {/* Error Message */} - {error && ( -
-

- {error} -

-
- )} - - {/* Success Message */} - {!error && !actionInProgress && ( -
-

- Operation completed successfully! -

-
- )} - - {/* Transaction Link */} - {transactionSignature && ( -
- - - - - View transaction on Solana Explorer - -
- )} -
- - {/* Footer */} -
- -
-
-
- ); -} diff --git a/apps/app/src/app/dashboard/manage/[address]/components/ActionSidebar.tsx b/apps/app/src/app/dashboard/manage/[address]/components/ActionSidebar.tsx deleted file mode 100644 index f94e8c4..0000000 --- a/apps/app/src/app/dashboard/manage/[address]/components/ActionSidebar.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Coins, Ban, Flame, ArrowRightLeft, Trash2 } from 'lucide-react'; - -interface ActionSidebarProps { - isPaused: boolean; - onTogglePause: () => void; - onMintTokens: () => void; - onForceTransfer: () => void; - onForceBurn?: () => void; - onRemoveFromStorage?: () => void; -} - -export function ActionSidebar({ - isPaused, - onTogglePause, - onMintTokens, - onForceTransfer, - onForceBurn, - onRemoveFromStorage, -}: ActionSidebarProps) { - return ( -
- - - Admin Actions - - - {/* */} - {/* */} - - - {onForceBurn && ( - - )} - - - - - - - Storage Management - Manage local token data - - - -

- This only removes the token from your browser's storage. The token will continue to exist - on the blockchain. -

-
-
-
- ); -} diff --git a/apps/app/src/app/dashboard/manage/[address]/components/AddressModal.tsx b/apps/app/src/app/dashboard/manage/[address]/components/AddressModal.tsx deleted file mode 100644 index 3cc9689..0000000 --- a/apps/app/src/app/dashboard/manage/[address]/components/AddressModal.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Button } from '@/components/ui/button'; - -interface AddressModalProps { - isOpen: boolean; - onClose: () => void; - onAdd: () => void; - newAddress: string; - onAddressChange: (address: string) => void; - title: string; - placeholder: string; - buttonText: string; -} - -export function AddressModal({ - isOpen, - onClose, - onAdd, - newAddress, - onAddressChange, - title, - placeholder, - buttonText, -}: AddressModalProps) { - if (!isOpen) return null; - - return ( -
-
-

{title}

-
-
- - onAddressChange(e.target.value)} - placeholder={placeholder} - className="w-full p-2 border rounded-md" - /> -
-
- - -
-
-
-
- ); -} diff --git a/apps/app/src/app/dashboard/manage/[address]/components/ForceBurnModal.tsx b/apps/app/src/app/dashboard/manage/[address]/components/ForceBurnModal.tsx deleted file mode 100644 index 39194a1..0000000 --- a/apps/app/src/app/dashboard/manage/[address]/components/ForceBurnModal.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { forceBurnTokens, type ForceBurnOptions } from '@/lib/management/force-burn'; -import { isAddress } from 'gill'; -import { TransactionSendingSigner } from '@solana/signers'; -import { ExternalLink, AlertCircle, Flame } from 'lucide-react'; - -interface ForceBurnModalProps { - isOpen: boolean; - onClose: () => void; - mintAddress: string; - permanentDelegate?: string; - transactionSendingSigner: TransactionSendingSigner; -} - -export function ForceBurnModal({ - isOpen, - onClose, - mintAddress, - permanentDelegate, - transactionSendingSigner, -}: ForceBurnModalProps) { - const [fromAddress, setFromAddress] = useState(''); - const [amount, setAmount] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - const [success, setSuccess] = useState(false); - const [transactionSignature, setTransactionSignature] = useState(''); - - if (!isOpen) return null; - - const validateSolanaAddress = (address: string) => { - return isAddress(address); - }; - - const validateAmount = (value: string) => { - const num = parseFloat(value); - return !isNaN(num) && num > 0; - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); - setSuccess(false); - setTransactionSignature(''); - - // Validate inputs - if (!fromAddress || !validateSolanaAddress(fromAddress)) { - setError('Please enter a valid source address'); - return; - } - - if (!amount || !validateAmount(amount)) { - setError('Please enter a valid amount'); - return; - } - - setIsLoading(true); - - try { - const options: ForceBurnOptions = { - mintAddress, - fromAddress, - amount, - permanentDelegate, - rpcUrl: 'https://api.devnet.solana.com', - }; - - const result = await forceBurnTokens(options, transactionSendingSigner); - - if (result.success && result.transactionSignature) { - setSuccess(true); - setTransactionSignature(result.transactionSignature); - // Reset form - setFromAddress(''); - setAmount(''); - } else { - setError(result.error || 'Force burn failed'); - } - } catch (err) { - setError(err instanceof Error ? err.message : 'An unexpected error occurred'); - } finally { - setIsLoading(false); - } - }; - - const handleClose = () => { - setFromAddress(''); - setAmount(''); - setError(''); - setSuccess(false); - setTransactionSignature(''); - onClose(); - }; - - return ( -
-
-
-
- -

Force Burn Tokens

-
- -
- - {/* Warning Banner */} -
-
- -
-

Warning: Irreversible Action

-

- Force burning will permanently destroy tokens from any account. This action cannot be - undone. Only use this for compliance or emergency purposes. -

-
-
-
- - {success ? ( -
-
- - - -
-

Tokens Burned Successfully!

-

- {amount} tokens have been permanently burned from the specified account. -

- {transactionSignature && ( - - )} -
- - -
-
- ) : ( -
-
-
- - setFromAddress(e.target.value)} - placeholder="Enter wallet or token account address" - className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700" - disabled={isLoading} - /> -

- The account from which tokens will be burned -

-
- -
- - setAmount(e.target.value)} - placeholder="0.00" - className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700" - disabled={isLoading} - /> -

- Number of tokens to permanently destroy -

-
- - {error && ( -
-

{error}

-
- )} - -
- - -
-
-
- )} - - {/* Additional Info */} - {!success && ( -
-

- Note: Only the permanent delegate authority can execute force burns. Ensure - you have the necessary permissions before attempting this action. -

-
- )} -
-
- ); -} diff --git a/apps/app/src/app/dashboard/manage/[address]/components/ForceBurnModalRefactored.tsx b/apps/app/src/app/dashboard/manage/[address]/components/ForceBurnModalRefactored.tsx deleted file mode 100644 index 6960740..0000000 --- a/apps/app/src/app/dashboard/manage/[address]/components/ForceBurnModalRefactored.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { forceBurnTokens, type ForceBurnOptions } from '@/lib/management/force-burn'; -import { TransactionSendingSigner } from '@solana/signers'; -import { Flame } from 'lucide-react'; - -import { BaseModal } from '@/components/shared/modals/BaseModal'; -import { TransactionSuccessView } from '@/components/shared/modals/TransactionSuccessView'; -import { WarningBanner } from '@/components/shared/modals/WarningBanner'; -import { SolanaAddressInput } from '@/components/shared/form/SolanaAddressInput'; -import { AmountInput } from '@/components/shared/form/AmountInput'; -import { useTransactionModal, useWalletConnection } from '@/hooks/useTransactionModal'; -import { useInputValidation } from '@/hooks/useInputValidation'; - -interface ForceBurnModalProps { - isOpen: boolean; - onClose: () => void; - mintAddress: string; - permanentDelegate?: string; - transactionSendingSigner: TransactionSendingSigner; -} - -export function ForceBurnModalRefactored({ - isOpen, - onClose, - mintAddress, - permanentDelegate, - transactionSendingSigner, -}: ForceBurnModalProps) { - const { walletAddress } = useWalletConnection(); - const { validateSolanaAddress, validateAmount } = useInputValidation(); - const { - isLoading, - error, - success, - transactionSignature, - setIsLoading, - setError, - setSuccess, - setTransactionSignature, - reset, - } = useTransactionModal(); - - const [fromAddress, setFromAddress] = useState(''); - const [amount, setAmount] = useState(''); - - const handleForceBurn = async () => { - if (!walletAddress) { - setError('Wallet not connected'); - return; - } - - if (!validateSolanaAddress(fromAddress)) { - setError('Please enter a valid source address'); - return; - } - - if (!validateAmount(amount)) { - setError('Please enter a valid amount'); - return; - } - - setIsLoading(true); - setError(''); - - try { - const options: ForceBurnOptions = { - mintAddress, - fromAddress, - amount, - permanentDelegate: permanentDelegate || walletAddress, - rpcUrl: 'https://api.devnet.solana.com', - }; - - const result = await forceBurnTokens(options, transactionSendingSigner); - - if (result.success && result.transactionSignature) { - setSuccess(true); - setTransactionSignature(result.transactionSignature); - } else { - setError(result.error || 'Force burn failed'); - } - } catch (err) { - setError(err instanceof Error ? err.message : 'An unexpected error occurred'); - } finally { - setIsLoading(false); - } - }; - - const handleClose = () => { - setFromAddress(''); - setAmount(''); - reset(); - onClose(); - }; - - const handleContinue = () => { - setFromAddress(''); - setAmount(''); - reset(); - }; - - return ( - -
- -

{success ? 'Force Burn Successful' : 'Force Burn Tokens'}

-
- - {success ? ( - - ) : ( -
- - - - - - - {permanentDelegate && ( -
- - -

- Only the permanent delegate can execute force burns -

-
- )} - - {error &&
{error}
} - -
- - -
-
- )} -
- ); -} diff --git a/apps/app/src/app/dashboard/manage/[address]/components/ForceTransferModal.tsx b/apps/app/src/app/dashboard/manage/[address]/components/ForceTransferModal.tsx deleted file mode 100644 index 609cb91..0000000 --- a/apps/app/src/app/dashboard/manage/[address]/components/ForceTransferModal.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import { useState, useContext } from 'react'; -import { Button } from '@/components/ui/button'; -import { forceTransferTokens, type ForceTransferOptions } from '@/lib/management/force-transfer'; -import { SelectedWalletAccountContext } from '@/context/SelectedWalletAccountContext'; -import { isAddress } from 'gill'; -import { TransactionSendingSigner } from '@solana/signers'; -import { ExternalLink, AlertCircle } from 'lucide-react'; - -interface ForceTransferModalProps { - isOpen: boolean; - onClose: () => void; - mintAddress: string; - permanentDelegate?: string; - transactionSendingSigner: TransactionSendingSigner; -} - -export function ForceTransferModal({ - isOpen, - onClose, - mintAddress, - permanentDelegate, - transactionSendingSigner, -}: ForceTransferModalProps) { - const [selectedWalletAccount] = useContext(SelectedWalletAccountContext); - const [fromAddress, setFromAddress] = useState(''); - const [toAddress, setToAddress] = useState(''); - const [amount, setAmount] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - const [success, setSuccess] = useState(false); - const [transactionSignature, setTransactionSignature] = useState(''); - - if (!isOpen) return null; - - const validateSolanaAddress = (address: string) => { - return isAddress(address); - }; - - const validateAmount = (amount: string) => { - const num = parseFloat(amount); - return !isNaN(num) && num > 0; - }; - - const handleForceTransfer = async () => { - if (!selectedWalletAccount?.address) { - setError('Wallet not connected'); - return; - } - - if (!validateSolanaAddress(fromAddress)) { - setError('Please enter a valid source Solana address'); - return; - } - - if (!validateSolanaAddress(toAddress)) { - setError('Please enter a valid destination Solana address'); - return; - } - - if (!validateAmount(amount)) { - setError('Please enter a valid amount'); - return; - } - - setIsLoading(true); - setError(''); - - try { - const walletAddress = selectedWalletAccount.address.toString(); - - const forceTransferOptions: ForceTransferOptions = { - mintAddress, - fromAddress, - toAddress, - amount, - permanentDelegate: permanentDelegate || walletAddress, - feePayer: walletAddress, - }; - - if (!transactionSendingSigner) { - throw new Error('Transaction signer not available'); - } - - const result = await forceTransferTokens(forceTransferOptions, transactionSendingSigner); - - if (result.success) { - setSuccess(true); - setTransactionSignature(result.transactionSignature || ''); - } else { - setError(result.error || 'Force transfer failed'); - } - } catch (err) { - setError(err instanceof Error ? err.message : 'An error occurred'); - } finally { - setIsLoading(false); - } - }; - - const handleClose = () => { - setFromAddress(''); - setToAddress(''); - setAmount(''); - setError(''); - setSuccess(false); - setTransactionSignature(''); - setIsLoading(false); - onClose(); - }; - - const handleAction = () => { - if (success) { - handleClose(); - } else { - handleForceTransfer(); - } - }; - - return ( -
-
-

- {success ? 'Force Transfer Successful' : 'Force Transfer Tokens'} -

- - {success ? ( -
-
-

Tokens transferred successfully!

-

Amount: {amount} tokens

-

- From: {fromAddress.slice(0, 8)}...{fromAddress.slice(-6)} -

-

- To: {toAddress.slice(0, 8)}...{toAddress.slice(-6)} -

-
- {transactionSignature && ( -
- -
- - -
-
- )} -
- ) : ( -
-
-
- -
-

Warning: Administrator Action

-

- This will force transfer tokens from any account without the owner's - permission. Use with caution. -

-
-
-
- -
- - setFromAddress(e.target.value)} - placeholder="Enter source wallet address..." - className="w-full p-2 border rounded-md" - /> - {fromAddress && !validateSolanaAddress(fromAddress) && ( -

Please enter a valid Solana address

- )} -
- -
- - setToAddress(e.target.value)} - placeholder="Enter destination wallet address..." - className="w-full p-2 border rounded-md" - /> - {toAddress && !validateSolanaAddress(toAddress) && ( -

Please enter a valid Solana address

- )} -
- -
- - setAmount(e.target.value)} - placeholder="Enter amount to transfer..." - step="0.000000001" - min="0" - className="w-full p-2 border rounded-md" - /> - {amount && !validateAmount(amount) && ( -

Please enter a valid positive amount

- )} -
- - {permanentDelegate && ( -
- - -
- )} - - {error &&
{error}
} -
- )} - -
- {!success && ( - - )} - -
-
-
- ); -} diff --git a/apps/app/src/app/dashboard/manage/[address]/components/ForceTransferModalRefactored.tsx b/apps/app/src/app/dashboard/manage/[address]/components/ForceTransferModalRefactored.tsx deleted file mode 100644 index f436bd4..0000000 --- a/apps/app/src/app/dashboard/manage/[address]/components/ForceTransferModalRefactored.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { forceTransferTokens, type ForceTransferOptions } from '@/lib/management/force-transfer'; -import { TransactionSendingSigner } from '@solana/signers'; -import { ArrowRightLeft } from 'lucide-react'; - -import { BaseModal } from '@/components/shared/modals/BaseModal'; -import { TransactionSuccessView } from '@/components/shared/modals/TransactionSuccessView'; -import { WarningBanner } from '@/components/shared/modals/WarningBanner'; -import { SolanaAddressInput } from '@/components/shared/form/SolanaAddressInput'; -import { AmountInput } from '@/components/shared/form/AmountInput'; -import { useTransactionModal, useWalletConnection } from '@/hooks/useTransactionModal'; -import { useInputValidation } from '@/hooks/useInputValidation'; - -interface ForceTransferModalProps { - isOpen: boolean; - onClose: () => void; - mintAddress: string; - permanentDelegate?: string; - transactionSendingSigner: TransactionSendingSigner; -} - -export function ForceTransferModalRefactored({ - isOpen, - onClose, - mintAddress, - permanentDelegate, - transactionSendingSigner, -}: ForceTransferModalProps) { - const { walletAddress } = useWalletConnection(); - const { validateSolanaAddress, validateAmount } = useInputValidation(); - const { - isLoading, - error, - success, - transactionSignature, - setIsLoading, - setError, - setSuccess, - setTransactionSignature, - reset, - } = useTransactionModal(); - - const [fromAddress, setFromAddress] = useState(''); - const [toAddress, setToAddress] = useState(''); - const [amount, setAmount] = useState(''); - - const handleForceTransfer = async () => { - if (!walletAddress) { - setError('Wallet not connected'); - return; - } - - if (!validateSolanaAddress(fromAddress)) { - setError('Please enter a valid source Solana address'); - return; - } - - if (!validateSolanaAddress(toAddress)) { - setError('Please enter a valid destination Solana address'); - return; - } - - if (!validateAmount(amount)) { - setError('Please enter a valid amount'); - return; - } - - setIsLoading(true); - setError(''); - - try { - const forceTransferOptions: ForceTransferOptions = { - mintAddress, - fromAddress, - toAddress, - amount, - permanentDelegate: permanentDelegate || walletAddress, - feePayer: walletAddress, - }; - - const result = await forceTransferTokens(forceTransferOptions, transactionSendingSigner); - - if (result.success) { - setSuccess(true); - setTransactionSignature(result.transactionSignature || ''); - } else { - setError(result.error || 'Force transfer failed'); - } - } catch (err) { - setError(err instanceof Error ? err.message : 'An error occurred'); - } finally { - setIsLoading(false); - } - }; - - const handleClose = () => { - setFromAddress(''); - setToAddress(''); - setAmount(''); - reset(); - onClose(); - }; - - const handleContinue = () => { - setFromAddress(''); - setToAddress(''); - setAmount(''); - reset(); - }; - - return ( - -
- -

- {success ? 'Force Transfer Successful' : 'Force Transfer Tokens'} -

-
- - {success ? ( - - ) : ( -
- - - - - - - - - {permanentDelegate && ( -
- - -
- )} - - {error &&
{error}
} - -
- - -
-
- )} -
- ); -} diff --git a/apps/app/src/app/dashboard/manage/[address]/components/MintModal.tsx b/apps/app/src/app/dashboard/manage/[address]/components/MintModal.tsx deleted file mode 100644 index d4fab47..0000000 --- a/apps/app/src/app/dashboard/manage/[address]/components/MintModal.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import { useState, useContext } from 'react'; -import { Button } from '@/components/ui/button'; -import { mintTokens, type MintOptions } from '@/lib/management/mint'; -import { SelectedWalletAccountContext } from '@/context/SelectedWalletAccountContext'; -import { isAddress } from 'gill'; -import { TransactionSendingSigner } from '@solana/signers'; -import { ExternalLink } from 'lucide-react'; - -interface MintModalProps { - isOpen: boolean; - onClose: () => void; - mintAddress: string; - mintAuthority?: string; - transactionSendingSigner: TransactionSendingSigner; -} - -export function MintModal({ isOpen, onClose, mintAddress, mintAuthority, transactionSendingSigner }: MintModalProps) { - const [selectedWalletAccount] = useContext(SelectedWalletAccountContext); - const [recipient, setRecipient] = useState(''); - const [amount, setAmount] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - const [success, setSuccess] = useState(false); - const [transactionSignature, setTransactionSignature] = useState(''); - - if (!isOpen) return null; - - const validateSolanaAddress = (address: string) => { - return isAddress(address); - }; - - const validateAmount = (amount: string) => { - const num = parseFloat(amount); - return !isNaN(num) && num > 0; - }; - - const handleMint = async () => { - if (!selectedWalletAccount?.address) { - setError('Wallet not connected'); - return; - } - - if (!validateSolanaAddress(recipient)) { - setError('Please enter a valid Solana address'); - return; - } - - if (!validateAmount(amount)) { - setError('Please enter a valid amount'); - return; - } - - setIsLoading(true); - setError(''); - - try { - const walletAddress = selectedWalletAccount.address.toString(); - - const mintOptions: MintOptions = { - mintAddress, - recipient, - amount, - mintAuthority: mintAuthority || walletAddress, - feePayer: walletAddress, - }; - - if (!transactionSendingSigner) { - throw new Error('Transaction signer not available'); - } - - const result = await mintTokens(mintOptions, transactionSendingSigner); - - if (result.success) { - setSuccess(true); - setTransactionSignature(result.transactionSignature || ''); - } else { - setError(result.error || 'Minting failed'); - } - } catch (err) { - setError(err instanceof Error ? err.message : 'An error occurred'); - } finally { - setIsLoading(false); - } - }; - - const handleClose = () => { - setRecipient(''); - setAmount(''); - setError(''); - setSuccess(false); - setTransactionSignature(''); - setIsLoading(false); - onClose(); - }; - - const handleAdd = () => { - if (success) { - handleClose(); - } else { - handleMint(); - } - }; - - return ( -
-
-

{success ? 'Mint Successful' : 'Mint Tokens'}

- - {success ? ( -
-
-

Tokens minted successfully!

-

- Amount: {amount} tokens to {recipient} -

-
- {transactionSignature && ( -
- -
- - -
-
- )} -
- ) : ( -
-
- - setRecipient(e.target.value)} - placeholder="Enter recipient Solana address..." - className="w-full p-2 border rounded-md" - /> - {recipient && !validateSolanaAddress(recipient) && ( -

Please enter a valid Solana address

- )} -
- -
- - setAmount(e.target.value)} - placeholder="Enter amount to mint..." - step="0.000000001" - min="0" - className="w-full p-2 border rounded-md" - /> - {amount && !validateAmount(amount) && ( -

Please enter a valid positive amount

- )} -
- - {mintAuthority && ( -
- - -
- )} - - {error &&
{error}
} -
- )} - -
- {!success && ( - - )} - -
-
-
- ); -} diff --git a/apps/app/src/app/dashboard/manage/[address]/components/MintModalRefactored.tsx b/apps/app/src/app/dashboard/manage/[address]/components/MintModalRefactored.tsx deleted file mode 100644 index 3b19e43..0000000 --- a/apps/app/src/app/dashboard/manage/[address]/components/MintModalRefactored.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { mintTokens, type MintOptions } from '@/lib/management/mint'; -import { TransactionSendingSigner } from '@solana/signers'; -import { Coins } from 'lucide-react'; - -import { BaseModal } from '@/components/shared/modals/BaseModal'; -import { TransactionSuccessView } from '@/components/shared/modals/TransactionSuccessView'; -import { SolanaAddressInput } from '@/components/shared/form/SolanaAddressInput'; -import { AmountInput } from '@/components/shared/form/AmountInput'; -import { useTransactionModal, useWalletConnection } from '@/hooks/useTransactionModal'; -import { useInputValidation } from '@/hooks/useInputValidation'; - -interface MintModalProps { - isOpen: boolean; - onClose: () => void; - mintAddress: string; - mintAuthority?: string; - transactionSendingSigner: TransactionSendingSigner; -} - -export function MintModalRefactored({ - isOpen, - onClose, - mintAddress, - mintAuthority, - transactionSendingSigner, -}: MintModalProps) { - const { walletAddress } = useWalletConnection(); - const { validateSolanaAddress, validateAmount } = useInputValidation(); - const { - isLoading, - error, - success, - transactionSignature, - setIsLoading, - setError, - setSuccess, - setTransactionSignature, - reset, - } = useTransactionModal(); - - const [recipient, setRecipient] = useState(''); - const [amount, setAmount] = useState(''); - - const handleMint = async () => { - if (!walletAddress) { - setError('Wallet not connected'); - return; - } - - if (!validateSolanaAddress(recipient)) { - setError('Please enter a valid Solana address'); - return; - } - - if (!validateAmount(amount)) { - setError('Please enter a valid amount'); - return; - } - - setIsLoading(true); - setError(''); - - try { - const mintOptions: MintOptions = { - mintAddress, - recipient, - amount, - mintAuthority: mintAuthority || walletAddress, - feePayer: walletAddress, - }; - - const result = await mintTokens(mintOptions, transactionSendingSigner); - - if (result.success) { - setSuccess(true); - setTransactionSignature(result.transactionSignature || ''); - } else { - setError(result.error || 'Minting failed'); - } - } catch (err) { - setError(err instanceof Error ? err.message : 'An error occurred'); - } finally { - setIsLoading(false); - } - }; - - const handleClose = () => { - setRecipient(''); - setAmount(''); - reset(); - onClose(); - }; - - const handleContinue = () => { - setRecipient(''); - setAmount(''); - reset(); - }; - - return ( - -
- -

{success ? 'Mint Successful' : 'Mint Tokens'}

-
- - {success ? ( - - ) : ( -
- - - - - {mintAuthority && ( -
- - -
- )} - - {error &&
{error}
} - -
- - -
-
- )} -
- ); -} diff --git a/apps/app/src/app/dashboard/manage/[address]/components/PauseConfirmModal.tsx b/apps/app/src/app/dashboard/manage/[address]/components/PauseConfirmModal.tsx deleted file mode 100644 index 2777ed9..0000000 --- a/apps/app/src/app/dashboard/manage/[address]/components/PauseConfirmModal.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { AlertTriangle, Loader2 } from 'lucide-react'; -import { Alert, AlertDescription } from '@/components/ui/alert'; - -interface PauseConfirmModalProps { - isOpen: boolean; - onClose: () => void; - onConfirm: () => Promise; - isPaused: boolean; - tokenName: string; - isLoading?: boolean; - error?: string; -} - -export function PauseConfirmModal({ - isOpen, - onClose, - onConfirm, - isPaused, - tokenName, - isLoading = false, - error, -}: PauseConfirmModalProps) { - const [isConfirming, setIsConfirming] = useState(false); - - const handleConfirm = async () => { - setIsConfirming(true); - try { - await onConfirm(); - } finally { - setIsConfirming(false); - } - }; - - const action = isPaused ? 'Unpause' : 'Pause'; - const actionContinuous = isPaused ? 'Unpausing' : 'Pausing'; - - if (!isOpen) return null; - - return ( -
-
-
- -

{action} Token

-
- -
- {isPaused ? ( - <> - You are about to unpause the token {tokenName}. This will - allow all token transfers to resume normally. - - ) : ( - <> - You are about to pause the token {tokenName}. This will - prevent all token transfers until the token is unpaused. - - )} -
- - - - - Important: This is a sensitive operation that affects all token holders. Make - sure you understand the implications before proceeding. - {!isPaused && ( - <> -
-
- When paused: -
    -
  • No token transfers will be possible
  • -
  • Token holders cannot send or receive tokens
  • -
  • Mint authority cannot mint new tokens
  • -
  • Freeze authority cannot freeze or thaw tokens
  • -
  • DeFi protocols may not function properly
  • -
  • Only the pause authority can unpause the token
  • -
- - )} -
-
- - {error && ( - - {error} - - )} - -
- - -
-
-
- ); -} diff --git a/apps/app/src/app/dashboard/manage/[address]/components/TokenAuthorities.tsx b/apps/app/src/app/dashboard/manage/[address]/components/TokenAuthorities.tsx deleted file mode 100644 index 8e023b3..0000000 --- a/apps/app/src/app/dashboard/manage/[address]/components/TokenAuthorities.tsx +++ /dev/null @@ -1,316 +0,0 @@ -import { useState, useEffect, useContext } from 'react'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Settings, Edit, Check, X, Loader2, ChevronDown, ChevronUp } from 'lucide-react'; -import { TokenDisplay } from '@/types/token'; -import { updateTokenAuthority } from '@/lib/management/authority'; -import { getTokenAuthorities } from '@/lib/solana/rpc'; -import { AuthorityType } from 'gill/programs/token'; -import { isAddress } from 'gill'; -import { useWalletAccountTransactionSendingSigner } from '@solana/react'; -import { ChainContext } from '@/context/ChainContext'; -import { SelectedWalletAccountContext } from '@/context/SelectedWalletAccountContext'; - -interface TokenAuthoritiesProps { - token: TokenDisplay; - setError: (error: string) => void; -} - -interface AuthorityInfo { - label: string; - role: AuthorityType | 'Metadata'; - currentAuthority?: string; - isEditing: boolean; - newAuthority: string; - isLoading: boolean; -} - -export function TokenAuthorities({ setError, token }: TokenAuthoritiesProps) { - // Create base authorities array - const baseAuthorities: AuthorityInfo[] = [ - { - label: 'Mint Authority', - role: AuthorityType.MintTokens, - currentAuthority: token.mintAuthority, - isEditing: false, - newAuthority: '', - isLoading: false, - }, - { - label: 'Freeze Authority', - role: AuthorityType.FreezeAccount, - currentAuthority: token.freezeAuthority, - isEditing: false, - newAuthority: '', - isLoading: false, - }, - { - label: 'Metadata Authority', - role: 'Metadata', - currentAuthority: token.metadataAuthority, - isEditing: false, - newAuthority: '', - isLoading: false, - }, - { - label: 'Pausable Authority', - role: AuthorityType.Pause, - currentAuthority: token.pausableAuthority, - isEditing: false, - newAuthority: '', - isLoading: false, - }, - { - label: 'Confidential Balances Authority', - role: AuthorityType.ConfidentialTransferMint, - currentAuthority: token.confidentialBalancesAuthority, - isEditing: false, - newAuthority: '', - isLoading: false, - }, - { - label: 'Permanent Delegate Authority', - role: AuthorityType.PermanentDelegate, - currentAuthority: token.permanentDelegateAuthority, - isEditing: false, - newAuthority: '', - isLoading: false, - }, - { - label: 'Scaled UI Amount Authority', - role: AuthorityType.ScaledUiAmount, - currentAuthority: token.scaledUiAmountAuthority, - isEditing: false, - newAuthority: '', - isLoading: false, - }, - ]; - - const [authorities, setAuthorities] = useState(baseAuthorities); - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - - const [isLoadingAuthorities, setIsLoadingAuthorities] = useState(false); - const [selectedWalletAccount] = useContext(SelectedWalletAccountContext); - const { chain: currentChain } = useContext(ChainContext); - - // Create transaction sending signer if wallet is connected - const transactionSendingSigner = useWalletAccountTransactionSendingSigner(selectedWalletAccount!, currentChain!); - - // Fetch current authorities from blockchain - useEffect(() => { - const fetchAuthorities = async () => { - if (!token.address) return; - - setIsLoadingAuthorities(true); - try { - const blockchainAuthorities = await getTokenAuthorities(token.address); - - setAuthorities(prev => - prev.map(auth => ({ - ...auth, - currentAuthority: - auth.role === AuthorityType.MintTokens - ? blockchainAuthorities.mintAuthority - : auth.role === AuthorityType.FreezeAccount - ? blockchainAuthorities.freezeAuthority - : auth.role === 'Metadata' - ? blockchainAuthorities.metadataAuthority - : auth.role === AuthorityType.Pause - ? blockchainAuthorities.pausableAuthority - : auth.role === AuthorityType.ConfidentialTransferMint - ? blockchainAuthorities.confidentialBalancesAuthority - : auth.role === AuthorityType.PermanentDelegate - ? blockchainAuthorities.permanentDelegateAuthority - : auth.role === AuthorityType.ScaledUiAmount - ? blockchainAuthorities.scaledUiAmountAuthority - : auth.currentAuthority, - })), - ); - } catch (error) { - setError(error instanceof Error ? error.message : 'Failed to fetch authorities'); - } finally { - setIsLoadingAuthorities(false); - } - }; - - fetchAuthorities(); - }, [token.address, setError]); - - const startEditing = (index: number) => { - setAuthorities(prev => - prev.map((auth, i) => - i === index - ? { - ...auth, - isEditing: true, - newAuthority: auth.currentAuthority || '', - } - : auth, - ), - ); - }; - - const cancelEditing = (index: number) => { - setAuthorities(prev => - prev.map((auth, i) => (i === index ? { ...auth, isEditing: false, newAuthority: '' } : auth)), - ); - }; - - const updateAuthority = async (index: number) => { - if (!token.address || !transactionSendingSigner) return; - - const authority = authorities[index]; - if (!authority.newAuthority.trim()) return; - - setAuthorities(prev => prev.map((auth, i) => (i === index ? { ...auth, isLoading: true } : auth))); - - try { - const result = await updateTokenAuthority( - { - mint: token.address, - role: authority.role, - newAuthority: authority.newAuthority.trim(), - rpcUrl: 'https://api.devnet.solana.com', - }, - transactionSendingSigner, - ); - - if (result.success) { - setAuthorities(prev => - prev.map((auth, i) => - i === index - ? { - ...auth, - currentAuthority: authority.newAuthority.trim(), - isEditing: false, - newAuthority: '', - isLoading: false, - } - : auth, - ), - ); - } else { - alert(`Failed to update authority: ${result.error}`); - } - } catch (error) { - setError(error instanceof Error ? error.message : 'Failed to update authority'); - } finally { - setAuthorities(prev => prev.map((auth, i) => (i === index ? { ...auth, isLoading: false } : auth))); - } - }; - - const validateSolanaAddress = (address: string) => { - return isAddress(address); - }; - - return ( - - - - - Manage the authorities for this token. Click edit to change an authority. -
-
-
- {isDropdownOpen && ( - -
- {authorities - .filter(authority => authority.currentAuthority) - .map((authority, index) => ( -
-
- - {!authority.isEditing && ( - - )} -
- - {authority.isEditing ? ( -
-
- ) => - setAuthorities(prev => - prev.map((auth, i) => - i === index - ? { ...auth, newAuthority: e.target.value } - : auth, - ), - ) - } - className="flex-1 h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" - /> - - -
- {authority.newAuthority && - !validateSolanaAddress(authority.newAuthority) && ( -

- Please enter a valid Solana address -

- )} -
- ) : ( - - {authority.currentAuthority?.slice(0, 8)}... - {authority.currentAuthority?.slice(-8)} - - )} -
- ))} -
- - {!selectedWalletAccount && ( -
-

Connect your wallet to manage authorities

-
- )} -
- )} -
- ); -} diff --git a/apps/app/src/app/dashboard/manage/[address]/components/TokenExtensions.tsx b/apps/app/src/app/dashboard/manage/[address]/components/TokenExtensions.tsx deleted file mode 100644 index fd60bc3..0000000 --- a/apps/app/src/app/dashboard/manage/[address]/components/TokenExtensions.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { useState, useContext } from 'react'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { updateScaledUiMultiplier } from '@/lib/management/scaledUiAmount'; -import { useWalletAccountTransactionSendingSigner } from '@solana/react'; -import { SelectedWalletAccountContext } from '@/context/SelectedWalletAccountContext'; -import { Settings, ChevronDown, ChevronUp } from 'lucide-react'; -import { TokenDisplay } from '@/types/token'; -import { ChainContext } from '@/context/ChainContext'; -import Link from 'next/link'; -import { UiWalletAccount } from '@wallet-standard/react'; - -interface TokenExtensionsProps { - token: TokenDisplay; -} - -export function TokenExtensions({ token }: TokenExtensionsProps) { - const [selectedWalletAccount] = useContext(SelectedWalletAccountContext); - const { chain: currentChain } = useContext(ChainContext); - - if (selectedWalletAccount && currentChain) { - return ( - - ); - } - - return ( -
-
-
- - - -
-

Manage Token Extensions

-

Manage the extensions enabled on this token

-
-
- - - - Wallet Required - Please connect your wallet to create a tokenized security - - -

- To manage token extensions, you need to connect a wallet first. Please use the wallet - connection button in the top navigation. -

-
-
-
-
- ); -} - -function ManageTokenExtensionsWithWallet({ - selectedWalletAccount, - currentChain, - token, -}: { - selectedWalletAccount: UiWalletAccount; - currentChain: string; - token: TokenDisplay; -}) { - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [showScaledUiEditor, setShowScaledUiEditor] = useState(false); - const [newMultiplier, setNewMultiplier] = useState(''); - const transactionSendingSigner = useWalletAccountTransactionSendingSigner( - selectedWalletAccount, - currentChain as `solana:${string}`, - ); - - return ( - - - - Extensions enabled on this token - - {isDropdownOpen && ( - -
- {token.extensions && token.extensions.length > 0 ? ( -
- {token.extensions.map((extension, index) => ( - - {extension} - - ))} -
- ) : ( -

No extensions configured

- )} - -
-
-

Metadata

-

Update token metadata and URI

-
- -
-
- {token.extensions?.includes('Scaled UI Amount') && ( -
-
-
-

Scaled UI Amount

-

- Update the UI amount multiplier for this mint -

-
- -
- {showScaledUiEditor && ( -
-
- - setNewMultiplier(e.target.value)} - placeholder="e.g., 1.5" - /> -
-
- -
-
- )} -
- )} -
- )} -
- ); -} diff --git a/apps/app/src/app/dashboard/manage/[address]/components/TokenOverview.tsx b/apps/app/src/app/dashboard/manage/[address]/components/TokenOverview.tsx deleted file mode 100644 index b769b52..0000000 --- a/apps/app/src/app/dashboard/manage/[address]/components/TokenOverview.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Coins, Copy, RefreshCw } from 'lucide-react'; -import { TokenDisplay } from '@/types/token'; -import { useContext, useEffect, useState, useCallback } from 'react'; -import { RpcContext } from '@/context/RpcContext'; -import { getTokenSupply } from '@/lib/utils'; -import { getTokenTypeLabel, getTokenPatternsLabel } from '@/lib/token/tokenTypeUtils'; -import { type Address } from 'gill'; - -interface TokenOverviewProps { - token: TokenDisplay; - copied: boolean; - onCopy: (text: string) => void; -} - -export function TokenOverview({ token, copied, onCopy }: TokenOverviewProps) { - const { rpc } = useContext(RpcContext); - const [currentSupply, setCurrentSupply] = useState(token.supply || '0'); - const [isLoadingSupply, setIsLoadingSupply] = useState(false); - - const fetchSupply = useCallback(async () => { - if (!token.address) return; - - setIsLoadingSupply(true); - try { - const supply = await getTokenSupply(rpc, token.address as Address); - setCurrentSupply(supply); - } catch { - // Silently handle errors and fall back to stored supply - setCurrentSupply(token.supply || '0'); - } finally { - setIsLoadingSupply(false); - } - }, [rpc, token.address, token.supply]); - - // Fetch supply on component mount - useEffect(() => { - fetchSupply(); - }, [fetchSupply]); - - const formatDate = (dateString?: string) => { - if (!dateString) return 'Unknown'; - return new Date(dateString).toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - }; - - return ( - - - - - Token Overview - - - -
-
- -

{token.name}

-
-
- -

{token.symbol}

-
-
- -
-

{isLoadingSupply ? 'Loading...' : currentSupply}

- -
-
-
- -

{token.decimals || '6'}

-
-
- -
-

{getTokenPatternsLabel(token.detectedPatterns)}

- {token.detectedPatterns && token.detectedPatterns.length > 1 && ( -
- {token.detectedPatterns.map((pattern, idx) => ( - - {getTokenTypeLabel(pattern)} - - ))} -
- )} -
-
-
- -

{formatDate(token.createdAt)}

-
-
- -
- -
- - {token.address} - - -
- {copied &&

Copied to clipboard!

} -
- - {token.transactionSignature && ( -
- -
- - {token.transactionSignature} - - -
-
- )} - - {token.metadataUri && ( -
- -

{token.metadataUri}

-
- )} -
-
- ); -} diff --git a/apps/app/src/app/dashboard/manage/[address]/components/TransferRestrictions.tsx b/apps/app/src/app/dashboard/manage/[address]/components/TransferRestrictions.tsx deleted file mode 100644 index 9d2fb48..0000000 --- a/apps/app/src/app/dashboard/manage/[address]/components/TransferRestrictions.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { useState } from 'react'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Settings, Plus, X, Shield, Ban, ChevronDown, ChevronUp } from 'lucide-react'; - -interface TransferRestrictionsProps { - accessList: string[]; - listType: 'allowlist' | 'blocklist'; - onAddToAccessList: () => void; - onRemoveFromAccessList: (address: string) => void; -} - -export function TransferRestrictions({ - accessList, - listType, - onAddToAccessList, - onRemoveFromAccessList, -}: TransferRestrictionsProps) { - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - - // Configuration for blocklist vs allowlist - const listConfig = { - blocklist: { - icon: Ban, - title: 'Blocklist', - badgeText: 'Stablecoin', - badgeClasses: 'bg-red-100 text-red-800', - iconClasses: 'text-red-600', - description: 'Block specific addresses from transferring this stablecoin', - emptyMessage: 'No addresses in blocklist', - }, - allowlist: { - icon: Shield, - title: 'Allowlist', - badgeText: 'Arcade Token', - badgeClasses: 'bg-green-100 text-green-800', - iconClasses: 'text-green-600', - description: 'Allow only specific addresses to transfer this arcade token', - emptyMessage: 'No addresses in allowlist', - }, - }; - - const config = listConfig[listType]; - const IconComponent = config.icon; - - const renderAddressList = () => { - if (accessList.length === 0) { - return

{config.emptyMessage}

; - } - - return ( -
- {accessList.map((addr, index) => ( -
- - {addr.slice(0, 8)}...{addr.slice(-8)} - - -
- ))} -
- ); - }; - - return ( - - - - Control who can transfer tokens - - {isDropdownOpen && ( - -
-
-
-
- -
{config.title}
- - {config.badgeText} - -
- -
-

{config.description}

- {renderAddressList()} -
-
-
- )} -
- ); -} diff --git a/apps/app/src/app/dashboard/manage/[address]/page.tsx b/apps/app/src/app/dashboard/manage/[address]/page.tsx deleted file mode 100644 index 0bcc2e9..0000000 --- a/apps/app/src/app/dashboard/manage/[address]/page.tsx +++ /dev/null @@ -1,515 +0,0 @@ -'use client'; - -import { useContext, useEffect, useState, useMemo, useRef } from 'react'; -import { Button } from '@/components/ui/button'; -import { ArrowLeft, ExternalLink } from 'lucide-react'; -import Link from 'next/link'; -import { useParams, useRouter } from 'next/navigation'; -import { TokenDisplay } from '@/types/token'; -import { Loader } from '@/components/ui/loader'; -import { findTokenByAddress } from '@/lib/token/tokenData'; -import { TokenStorage } from '@/lib/token/tokenStorage'; -import { getTokenPatternsLabel } from '@/lib/token/tokenTypeUtils'; -import { SelectedWalletAccountContext } from '@/context/SelectedWalletAccountContext'; -import { ChainContext } from '@/context/ChainContext'; -import { TokenOverview } from './components/TokenOverview'; -import { TokenAuthorities } from './components/TokenAuthorities'; -import { TokenExtensions } from './components/TokenExtensions'; -import { TransferRestrictions } from './components/TransferRestrictions'; -import { ActionSidebar } from './components/ActionSidebar'; -import { AddressModal } from './components/AddressModal'; -import { MintModalRefactored as MintModal } from './components/MintModalRefactored'; -import { ForceTransferModalRefactored as ForceTransferModal } from './components/ForceTransferModalRefactored'; -import { ForceBurnModalRefactored as ForceBurnModal } from './components/ForceBurnModalRefactored'; -import { ActionResultModal } from './components/ActionResultModal'; -import { PauseConfirmModal } from './components/PauseConfirmModal'; -import { useWalletAccountTransactionSendingSigner } from '@solana/react'; -import { - addAddressToBlocklist, - addAddressToAllowlist, - removeAddressFromBlocklist, - removeAddressFromAllowlist, -} from '@/lib/management/accessList'; -import { Address, createSolanaRpc, Rpc, SolanaRpcApi } from 'gill'; -import { getList, getListConfigPda, getTokenExtensions } from '@mosaic/sdk'; -import { Mode } from '@token-acl/abl-sdk'; -import { pauseTokenWithWallet, unpauseTokenWithWallet, checkTokenPauseState } from '@/lib/management/pause'; - -export default function ManageTokenPage() { - const [selectedWalletAccount] = useContext(SelectedWalletAccountContext); - const params = useParams(); - const address = params.address as string; - - if (!selectedWalletAccount) { - return ( -
-
-

Wallet Required

-

Please connect your Solana wallet to manage tokens.

-
-
- ); - } - - return ; -} - -const getAccessList = async ( - rpc: Rpc, - authority: Address, - mint: Address, -): Promise<{ type: 'allowlist' | 'blocklist'; wallets: string[] }> => { - const listConfigPda = await getListConfigPda({ - authority, - mint, - }); - const list = await getList({ rpc, listConfig: listConfigPda }); - return { - type: list.mode === Mode.Allow ? 'allowlist' : 'blocklist', - wallets: list.wallets, - }; -}; - -function ManageTokenConnected({ address }: { address: string }) { - const router = useRouter(); - const [selectedWalletAccount] = useContext(SelectedWalletAccountContext); - const { chain: currentChain, solanaRpcUrl } = useContext(ChainContext); - const [token, setToken] = useState(null); - const [loading, setLoading] = useState(true); - const [copied, setCopied] = useState(false); - const [accessList, setAccessList] = useState([]); - const [listType, setListType] = useState<'allowlist' | 'blocklist'>('blocklist'); - const [newAddress, setNewAddress] = useState(''); - const [showAccessListModal, setShowAccessListModal] = useState(false); - const [showMintModal, setShowMintModal] = useState(false); - const [showForceTransferModal, setShowForceTransferModal] = useState(false); - const [showForceBurnModal, setShowForceBurnModal] = useState(false); - const [isPaused, setIsPaused] = useState(false); - const [showPauseModal, setShowPauseModal] = useState(false); - const [pauseError, setPauseError] = useState(''); - const [actionInProgress, setActionInProgress] = useState(false); - const [error, setError] = useState(''); - const [transactionSignature, setTransactionSignature] = useState(''); - const [refreshTrigger, setRefreshTrigger] = useState(0); - - const rpc = useMemo(() => createSolanaRpc(solanaRpcUrl) as Rpc, [solanaRpcUrl]); - - const loadedAccessListRef = useRef(null); - - const refreshAccessList = () => { - setTimeout(() => { - loadedAccessListRef.current = null; - setRefreshTrigger(prev => prev + 1); - }, 600); - }; - - const transactionSendingSigner = useWalletAccountTransactionSendingSigner(selectedWalletAccount!, currentChain!); - - useEffect(() => { - const addTokenExtensionsToFoundToken = async (foundToken: TokenDisplay): Promise => { - const extensions = await getTokenExtensions(rpc, foundToken.address as Address); - foundToken.extensions = extensions; - setToken(foundToken); - - if (foundToken.address) { - const pauseState = await checkTokenPauseState(foundToken.address, solanaRpcUrl); - setIsPaused(pauseState); - } - }; - - const loadTokenData = () => { - const foundToken = findTokenByAddress(address); - - if (foundToken) { - setToken(foundToken); - addTokenExtensionsToFoundToken(foundToken); - } - - setLoading(false); - }; - - loadTokenData(); - }, [address, rpc, solanaRpcUrl]); - - useEffect(() => { - const loadAccessList = async () => { - const currentKey = `${selectedWalletAccount?.address}-${token?.address}-${solanaRpcUrl}-${refreshTrigger}`; - - if (loadedAccessListRef.current === currentKey) { - return; - } - - const accessList = await getAccessList( - rpc, - selectedWalletAccount?.address as Address, - token?.address as Address, - ); - setAccessList(accessList.wallets); - setListType(accessList.type); - loadedAccessListRef.current = currentKey; - }; - - if (rpc && selectedWalletAccount?.address && token?.address && token?.isSrfc37) { - loadAccessList(); - } - }, [rpc, selectedWalletAccount?.address, token?.address, token?.isSrfc37, solanaRpcUrl, refreshTrigger]); - - const copyToClipboard = async (text: string) => { - try { - await navigator.clipboard.writeText(text); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } catch {} - }; - - const openInExplorer = () => { - window.open(`https://explorer.solana.com/address/${address}?cluster=devnet`, '_blank'); - }; - - const addToAccessList = async () => { - setShowAccessListModal(false); - if (newAddress.trim() && accessList.includes(newAddress.trim())) { - setError('Address already in list'); - return; - } - - await handleAddToAccessList(token?.address || '', newAddress.trim()); - setNewAddress(''); - }; - - const removeFromAccessList = async (address: string) => { - if (!selectedWalletAccount?.address || !token?.address || !transactionSendingSigner) { - setError('Required parameters not available'); - return; - } - - setActionInProgress(true); - setError(''); - - try { - let result; - if (listType === 'blocklist') { - result = await removeAddressFromBlocklist( - rpc, - { - mintAddress: token.address, - walletAddress: address, - }, - transactionSendingSigner, - ); - } else { - result = await removeAddressFromAllowlist( - rpc, - { - mintAddress: token.address, - walletAddress: address, - }, - transactionSendingSigner, - ); - } - - if (result.success) { - setTransactionSignature(result.transactionSignature || ''); - refreshAccessList(); - } else { - setError(result.error || 'Removal failed'); - } - } catch (err) { - setError(err instanceof Error ? err.message : 'An error occurred'); - } finally { - setActionInProgress(false); - } - }; - - const handleAddToAccessList = async (mintAddress: string, address: string) => { - if (!selectedWalletAccount?.address) { - setError('Wallet not connected'); - return; - } - - setActionInProgress(true); - setError(''); - - try { - if (!transactionSendingSigner) { - throw new Error('Transaction signer not available'); - } - - let result; - if (listType === 'blocklist') { - result = await addAddressToBlocklist( - rpc, - { - mintAddress, - walletAddress: address, - }, - transactionSendingSigner, - ); - } else { - result = await addAddressToAllowlist( - rpc, - { - mintAddress, - walletAddress: address, - }, - transactionSendingSigner, - ); - } - - if (result.success) { - setTransactionSignature(result.transactionSignature || ''); - refreshAccessList(); - } else { - setError(result.error || 'Operation failed'); - } - } catch (err) { - setError(err instanceof Error ? err.message : 'An error occurred'); - } finally { - setActionInProgress(false); - } - }; - - const handleRemoveFromStorage = () => { - if ( - confirm( - 'Are you sure you want to remove this token from your local storage? This only removes it from your browser - the token will continue to exist on the blockchain.', - ) - ) { - TokenStorage.removeToken(address); - router.push('/dashboard'); - } - }; - - const togglePause = async () => { - if (!selectedWalletAccount?.address || !token?.address || !transactionSendingSigner) { - setError('Required parameters not available'); - return; - } - - // Check if the connected wallet has pause authority - const walletAddress = selectedWalletAccount.address.toString(); - if (token.pausableAuthority !== walletAddress) { - setPauseError( - 'Connected wallet does not have pause authority. Only the pause authority can pause/unpause this token.', - ); - setShowPauseModal(true); - return; - } - - // Show confirmation modal - setShowPauseModal(true); - }; - - const handlePauseConfirm = async () => { - if (!selectedWalletAccount?.address || !token?.address || !transactionSendingSigner) { - setPauseError('Required parameters not available'); - return; - } - - setActionInProgress(true); - setPauseError(''); - - try { - const result = isPaused - ? await unpauseTokenWithWallet( - { - mintAddress: token.address, - pauseAuthority: token.pausableAuthority, - feePayer: selectedWalletAccount.address.toString(), - rpcUrl: solanaRpcUrl, - }, - transactionSendingSigner, - ) - : await pauseTokenWithWallet( - { - mintAddress: token.address, - pauseAuthority: token.pausableAuthority, - feePayer: selectedWalletAccount.address.toString(), - rpcUrl: solanaRpcUrl, - }, - transactionSendingSigner, - ); - - if (result.success) { - setTransactionSignature(result.transactionSignature || ''); - setIsPaused(result.paused ?? !isPaused); - setShowPauseModal(false); - - // Update token in local storage - const storedTokens = JSON.parse(localStorage.getItem('mosaic_tokens') || '[]') as TokenDisplay[]; - const updatedTokens = storedTokens.map(t => { - if (t.address === token.address) { - return { ...t, isPaused: result.paused ?? !isPaused }; - } - return t; - }); - localStorage.setItem('mosaic_tokens', JSON.stringify(updatedTokens)); - } else { - setPauseError(result.error || 'Operation failed'); - } - } catch (err) { - setPauseError(err instanceof Error ? err.message : 'An error occurred'); - } finally { - setActionInProgress(false); - } - }; - - if (loading) { - return ( -
-
-
- -

Loading token details...

-
-
-
- ); - } - - if (!token) { - return ( -
-
-
-

Token Not Found

-

- The token with address {address} could not be found in your local storage. -

- - - -
-
-
- ); - } - - return ( -
-
- {/* Header */} -
-
- - - -
-

{token.name}

-

- Manage your {getTokenPatternsLabel(token.detectedPatterns)} token -

-
-
-
- -
-
- -
- {/* Main Content */} -
- - - setShowAccessListModal(true)} - onRemoveFromAccessList={removeFromAccessList} - /> - -
- - {/* Sidebar */} - setShowMintModal(true)} - onForceTransfer={() => setShowForceTransferModal(true)} - onForceBurn={() => setShowForceBurnModal(true)} - onRemoveFromStorage={handleRemoveFromStorage} - /> -
-
- - {/* Modals */} - { - setError(''); - setTransactionSignature(''); - setActionInProgress(false); - }} - actionInProgress={actionInProgress} - error={error} - transactionSignature={transactionSignature} - /> - - { - setShowAccessListModal(false); - setNewAddress(''); - }} - onAdd={addToAccessList} - newAddress={newAddress} - onAddressChange={setNewAddress} - title={`Add to ${listType === 'allowlist' ? 'Allowlist' : 'Blocklist'}`} - placeholder="Enter Solana address..." - buttonText={`Add to ${listType === 'allowlist' ? 'Allowlist' : 'Blocklist'}`} - /> - - {transactionSendingSigner && ( - setShowMintModal(false)} - mintAddress={address} - mintAuthority={token?.mintAuthority} - transactionSendingSigner={transactionSendingSigner} - /> - )} - - {transactionSendingSigner && ( - setShowForceTransferModal(false)} - mintAddress={address} - permanentDelegate={token?.permanentDelegateAuthority} - transactionSendingSigner={transactionSendingSigner} - /> - )} - - {transactionSendingSigner && ( - setShowForceBurnModal(false)} - mintAddress={address} - permanentDelegate={token?.permanentDelegateAuthority} - transactionSendingSigner={transactionSendingSigner} - /> - )} - - { - setShowPauseModal(false); - setPauseError(''); - }} - onConfirm={handlePauseConfirm} - isPaused={isPaused} - tokenName={token?.name || 'Token'} - isLoading={actionInProgress} - error={pauseError} - /> -
- ); -} diff --git a/apps/app/src/app/dashboard/page.tsx b/apps/app/src/app/dashboard/page.tsx deleted file mode 100644 index eaa297d..0000000 --- a/apps/app/src/app/dashboard/page.tsx +++ /dev/null @@ -1,136 +0,0 @@ -'use client'; - -import { useContext, useEffect, useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { Plus, Coins, Upload } from 'lucide-react'; -import { useRouter } from 'next/navigation'; -import Link from 'next/link'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { TokenDisplay } from '@/types/token'; -import { Loader } from '@/components/ui/loader'; -import { getAllTokens, getTokenCount } from '@/lib/token/tokenData'; -import { TokenStorage } from '@/lib/token/tokenStorage'; -import { SelectedWalletAccountContext } from '@/context/SelectedWalletAccountContext'; -import { TokenCard } from './components/TokenCard'; - -export default function DashboardPage() { - const [selectedWalletAccount] = useContext(SelectedWalletAccountContext); - - return selectedWalletAccount ? : ; -} - -function DashboardConnected() { - const [tokens, setTokens] = useState([]); - const [loading, setLoading] = useState(true); - const router = useRouter(); - - useEffect(() => { - // Load tokens from local storage - const loadTokens = () => { - const storedTokens = getAllTokens(); - setTokens(storedTokens); - setLoading(false); - }; - - loadTokens(); - }, []); - - useEffect(() => { - if (!loading && tokens.length === 0) { - router.push('/dashboard/create'); - } - }, [loading, tokens, router]); - - const handleDeleteToken = (address: string) => { - if (confirm('Are you sure you want to remove this token from your local storage?')) { - TokenStorage.removeToken(address); - setTokens(getAllTokens()); - } - }; - - if (loading) { - return ( -
-
-
- -

Loading your tokens...

-
-
-
- ); - } - - if (!loading && tokens.length === 0) return null; - - return ( -
-
-
-
-

Your Tokens

-

- Manage your created tokens and their extensions ({getTokenCount()} total) -

-
- - - - - - - - - Stablecoin - - - - - - Arcade Token - - - - - - Tokenized Security - - - - - - Import Existing Token - - - - -
- -
- {tokens.map((token, index) => ( - - ))} -
-
-
- ); -} - -function DashboardDisconnected() { - return ( -
-
-

Welcome to Mosaic

-

Please connect your Solana wallet to access the dashboard.

-
-
- ); -} diff --git a/apps/app/src/app/docs/deps/page.tsx b/apps/app/src/app/docs/deps/page.tsx deleted file mode 100644 index 7e038d5..0000000 --- a/apps/app/src/app/docs/deps/page.tsx +++ /dev/null @@ -1,71 +0,0 @@ -export default function DocsDepsPage() { - return ( -
-

SDK, CLI, and Dependencies

-

Key packages and where to find their READMEs in this repository.

- -

Internal packages (paths)

-
    -
  • - packages/sdk/README.md — SDK developer guide -
  • -
  • - packages/cli/README.md — CLI usage and commands -
  • -
  • - apps/app/README.md — Dashboard app user and developer guide -
  • -
  • - packages/abl/README.md — Address-Based Lists (SRFC-37) bindings -
  • -
  • - packages/token-acl/README.md — Token ACL program bindings -
  • -
  • - packages/tlv-account-resolution/README.md — TLV helpers -
  • -
- -

How the UI uses them

-
    -
  • - Mint creation and management: @mosaic/sdk -
  • -
  • - Access lists (allow/block): @mosaic/abl -
  • -
  • - Freeze/thaw and permissionless thaw: @mosaic/token-acl - (Token ACL) -
  • -
  • - RPC and types: gill and wallet adapters -
  • -
- -

External references

- -
- ); -} diff --git a/apps/app/src/app/docs/layout.tsx b/apps/app/src/app/docs/layout.tsx deleted file mode 100644 index bbeb75f..0000000 --- a/apps/app/src/app/docs/layout.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import Link from 'next/link'; -import { cn } from '@/lib/utils'; - -export default function DocsLayout({ children }: { children: React.ReactNode }) { - const nav = [ - { href: '/docs', label: 'Overview' }, - { href: '/docs/website', label: 'About the App' }, - { href: '/docs/templates', label: 'Token Templates' }, - { href: '/docs/deps', label: 'SDK, CLI, and Deps' }, - ]; - - return ( -
-
- -
-
{children}
-
-
-
- ); -} diff --git a/apps/app/src/app/docs/page.tsx b/apps/app/src/app/docs/page.tsx deleted file mode 100644 index 30e7cc4..0000000 --- a/apps/app/src/app/docs/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -export default function DocsOverviewPage() { - return ( -
-

Mosaic Docs

-

- Mosaic is a tokenization engine for Solana. This site lets you create and manage Token-2022 mints using - prebuilt templates (Stablecoin, Arcade Token, Tokenized Security) and operate them with modern controls. -

-

What's inside

-
    -
  • Visual token creation flows with smart defaults
  • -
  • Dashboards to manage authorities, access lists, and account state
  • -
  • Wallet integration for signing and submission
  • -
-
- ); -} diff --git a/apps/app/src/app/docs/templates/page.tsx b/apps/app/src/app/docs/templates/page.tsx deleted file mode 100644 index b45d23f..0000000 --- a/apps/app/src/app/docs/templates/page.tsx +++ /dev/null @@ -1,146 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { CapabilityKey, ExtensionKey } from '@/components/capabilities/registry'; -import { DollarSign, Gamepad2, CandlestickChart, ChevronDown } from 'lucide-react'; -import { CreateTemplateSidebar } from '@/components/CreateTemplateSidebar'; - -export default function DocsTemplatesPage() { - return ( -
-

Token Templates

-

- Mosaic provides three templates that compose Token-2022 extensions for common use-cases. New token - accounts start frozen and are thawed according to your access-control rules (sRFC-37 Token ACL + - standard allow/block list program). -

- - - -

Single-signer side-effects

-

- If the fee payer equals the mint authority, creation flows also set up Token ACL config, set the gating - program to ABL, create the ABL list, set extra metas on the mint, and enable permissionless thaw. -

-
- ); -} - -function TemplatesAccordion() { - const [open, setOpen] = useState(null); - - type Template = { - id: string; - title: string; - Icon: React.ComponentType<{ className?: string }>; - summary: string; - badges: string[]; - capabilities: CapabilityKey[]; - extensions: ExtensionKey[]; - }; - - const templates: Template[] = [ - { - id: 'stablecoin', - title: 'Stablecoin', - Icon: DollarSign, - summary: - 'Compliance-oriented mint with strong controls and optional privacy. Defaults to a blocklist model; switch to allowlist for closed-loop.', - badges: ['Metadata', 'Pausable', 'Default Account State', 'Confidential Transfer', 'Permanent Delegate'], - capabilities: ['metadata', 'accessControls', 'pausable', 'permanentDelegate', 'confidentialBalances'], - extensions: [ - 'extMetadata', - 'extPausable', - 'extDefaultAccountStateAllowOrBlock', - 'extConfidentialBalances', - 'extPermanentDelegate', - ], - }, - { - id: 'arcade', - title: 'Arcade Token', - Icon: Gamepad2, - summary: - 'Closed-loop (allowlist-only) mint for games and apps. Accounts must be explicitly allowed before holding or receiving tokens.', - badges: ['Metadata', 'Pausable', 'Default Account State (Allowlist)', 'Permanent Delegate'], - capabilities: ['closedLoopAllowlistOnly', 'pausable', 'metadata', 'permanentDelegate'], - extensions: ['extMetadata', 'extPausable', 'extDefaultAccountStateAllow', 'extPermanentDelegate'], - }, - { - id: 'security', - title: 'Tokenized Security', - Icon: CandlestickChart, - summary: - 'Stablecoin feature set plus Scaled UI Amount; display UI-friendly amounts while keeping on-chain units consistent for accounting.', - badges: [ - 'Metadata', - 'Pausable', - 'Default Account State', - 'Confidential Transfer', - 'Permanent Delegate', - 'Scaled UI Amount', - ], - capabilities: [ - 'metadata', - 'accessControls', - 'pausable', - 'permanentDelegate', - 'confidentialBalances', - 'scaledUIAmount', - ], - extensions: [ - 'extMetadata', - 'extPausable', - 'extDefaultAccountStateAllowOrBlock', - 'extConfidentialBalances', - 'extPermanentDelegate', - 'extScaledUIAmount', - ], - }, - ]; - - return ( -
- {templates.map(t => ( - - - -

{t.summary}

-
- {t.badges.map(b => ( - - {b} - - ))} -
- {open === t.id && ( -
- -
- )} -
-
- ))} -
- ); -} diff --git a/apps/app/src/app/docs/website/page.tsx b/apps/app/src/app/docs/website/page.tsx deleted file mode 100644 index 7f36f51..0000000 --- a/apps/app/src/app/docs/website/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -export default function DocsWebsitePage() { - return ( -
-

About the App

-

- Mosaic UI is a Next.js app that uses gill, @solana/kit, and{' '} - @mosaic/sdk for on-chain operations. -

-

Architecture

-
    -
  • Providers: wallet, theme, cluster (devnet/testnet/mainnet), RPC
  • -
  • Pages: Landing, Dashboard, Create flows, Manage token pages
  • -
  • Local storage keeps a list of tokens you created
  • -
-

Create flows

-
    -
  • Stablecoin: metadata, pausable, confidential balances, permanent delegate
  • -
  • Arcade Token: metadata, pausable, permanent delegate, allowlist
  • -
  • Tokenized Security: stablecoin set + scaled UI amount
  • -
  • ACL mode: choose allowlist (closed-loop) or blocklist
  • -
  • Single-signer side-effects: Token ACL + ABL set up automatically
  • -
-

Clusters and RPC

-

- The app defaults to Devnet. Use the cluster selector in the header to switch. RPC and subscriptions come - from the cluster context. -

-

Wallets

-

- Connect a Solana wallet supported by @solana/wallet-adapter. You need SOL for fees on the - chosen cluster. -

-
- ); -} diff --git a/apps/app/src/app/globals.css b/apps/app/src/app/globals.css index 0d0fb6b..58c97c8 100644 --- a/apps/app/src/app/globals.css +++ b/apps/app/src/app/globals.css @@ -1,160 +1,803 @@ @import 'tailwindcss'; +@custom-variant dark (&:is(.dark *)); + +/* Inline code styling - sand background for code wrapped in `` */ +.prose :not(pre) > code, +.prose p code, +.prose li code, +.prose td code, +.prose th code { + background-color: oklch(0.927 0.0122 96.43) !important; /* sand-200 */ + color: oklch(0.279 0.0029 17.33) !important; /* sand-1500 */ + padding: 0.125rem 0.375rem !important; + border-radius: 0.375rem !important; + font-size: 0.75rem !important; /* text-xs */ + border: 1px solid oklch(0.839 0.0187 78.23) !important; /* sand-400 */ + font-family: var(--font-berkeley-mono), ui-monospace, monospace !important; +} + +/* Ensure code blocks stay clean - NO background or styling */ +.prose pre code, +.prose pre code *, +.prose code[class*='language-'], +.prose code[class*='shiki'], +.prose code[class*='hljs'], +.prose code[class*='prism'] { + background: transparent !important; + background-color: transparent !important; + border: none !important; + padding: 0 !important; +} + +/* Add padding to code blocks */ +.prose pre { + padding-left: 1rem !important; + padding-right: 1.5rem !important; +} + @theme { - /* Define custom colors for Tailwind utilities */ - --color-background: hsl(var(--background)); - --color-foreground: hsl(var(--foreground)); - --color-card: hsl(var(--card)); - --color-card-foreground: hsl(var(--card-foreground)); - --color-popover: hsl(var(--popover)); - --color-popover-foreground: hsl(var(--popover-foreground)); - --color-primary: hsl(var(--primary)); - --color-primary-foreground: hsl(var(--primary-foreground)); - --color-secondary: hsl(var(--secondary)); - --color-secondary-foreground: hsl(var(--secondary-foreground)); - --color-muted: hsl(var(--muted)); - --color-muted-foreground: hsl(var(--muted-foreground)); - --color-accent: hsl(var(--accent)); - --color-accent-foreground: hsl(var(--accent-foreground)); - --color-destructive: hsl(var(--destructive)); - --color-destructive-foreground: hsl(var(--destructive-foreground)); - --color-border: hsl(var(--border)); - --color-input: hsl(var(--input)); - --color-ring: hsl(var(--ring)); + --color-cream: oklch(0.9553 0.0029 84.56); + --color-bg1: oklch(97.4% 0.004 286.33); + + /* Border colors mapped to sand scale */ + --color-border-strong: oklch(0.839 0.0187 78.23); /* sand/400 */ + --color-border-medium: oklch(0.884 0.0163 82.79); /* sand/300 */ + --color-border-low: oklch(0.927 0.0122 96.43); /* sand/200 */ + --color-border-extra-low: oklch(0.9665 0.0067 97.35); /* sand/100 */ + + --color-sand-100: oklch(0.9665 0.0067 97.35); + --color-sand-200: oklch(0.927 0.0122 96.43); + --color-sand-300: oklch(0.884 0.0163 82.79); + --color-sand-400: oklch(0.839 0.0187 78.23); + --color-sand-500: oklch(0.7935 0.0213 74.62); + --color-sand-600: oklch(0.7454 0.0237 67.45); + --color-sand-700: oklch(0.6938 0.0247 56.8); + --color-sand-800: oklch(0.6436 0.0256 48.3); + --color-sand-900: oklch(0.5916 0.0259 41.3); + --color-sand-1000: oklch(0.5438 0.0206 39.32); + --color-sand-1100: oklch(0.4947 0.0156 41.09); + --color-sand-1200: oklch(0.442 0.0111 34.3); + --color-sand-1300: oklch(0.3909 0.0076 43.22); + --color-sand-1400: oklch(0.3356 0.0046 39.42); + --color-sand-1500: oklch(0.279 0.0029 17.33); + --color-sand-1600: oklch(0.2189 0.0016 17.28); + + --color-highlight: oklch(1 0 0); + + --color-black: oklch(0 0 0); + --color-white: oklch(1 0 0); + --color-gray-900: oklch(0.1725 0.0078 264.05); + --color-gray-800: oklch(0.2706 0.0141 264.05); + --color-gray-600: oklch(0.4569 0.0171 264.05); + --color-gray-500: oklch(0.5137 0.0171 264.05); + --color-gray-400: oklch(0.6196 0.0171 264.05); } -@layer base { - :root { - --background: 222.2 100% 99.5%; - --foreground: 222.2 84% 4.9%; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - --primary: 255 80% 40%; - --primary-foreground: 255 0% 100%; - --secondary: 210 40% 96%; - --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96%; - --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96%; - --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; - --radius: 0.5rem; - --mosaic-gradient-color: #000; - } - - .dark { - --background: 222.2 84% 3%; - --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - --primary: 255 80% 55%; - --primary-foreground: 255 0% 100%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; - --mosaic-gradient-color: #fff; +:root { + /* Title sizes */ + --text-h1-mobile: 36px; + --text-h1-desktop: 64px; + --text-title-2-mobile: 36px; + --text-title-2-desktop: 56px; + --text-h2-mobile: 29px; + --text-h2-desktop: 44px; + --text-title-4-mobile: 24px; + --text-title-4-desktop: 32px; + --text-title-5: 21px; + + /* Body sizes */ + --text-body-xl: 19px; + --text-body-l: 16px; + --text-body-md: 14px; + + /* UI sizes */ + --text-nav-item: 14px; + + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); + --diagonal-pattern-color: oklch(0.927 0.0122 96.43 / 0.5); +} + +/* Responsive typography classes matching Figma specs */ + +/* Title/1: 64px, weight 500, line-height 1.03, tracking -2.88px */ +.text-h1 { + font-family: var(--font-abc-diatype); + font-size: var(--text-h1-mobile); + font-weight: 500; + line-height: 1.03; + letter-spacing: -2.88px; +} + +@media (min-width: 768px) { + .text-h1 { + font-size: var(--text-h1-desktop); + } +} + +/* Title/2: 56px, weight 500, line-height 1.05, tracking -2.52px */ +.text-title-2 { + font-family: var(--font-abc-diatype); + font-size: var(--text-title-2-mobile); + font-weight: 500; + line-height: 1.05; + letter-spacing: -2.52px; +} + +@media (min-width: 768px) { + .text-title-2 { + font-size: var(--text-title-2-desktop); + } +} + +/* Title/3: 44px, weight 500, line-height 1.1, tracking -1.76px */ +.text-h2 { + font-family: var(--font-abc-diatype); + font-size: var(--text-h2-mobile); + font-weight: 500; + line-height: 1.1; + letter-spacing: -1.76px; +} + +@media (min-width: 768px) { + .text-h2 { + font-size: var(--text-h2-desktop); + } +} + +/* Title/4: 32px, weight 500, line-height 1.1, tracking -1.28px */ +.text-title-4 { + font-family: var(--font-abc-diatype); + font-size: var(--text-title-4-mobile); + font-weight: 500; + line-height: 1.1; + letter-spacing: -1.28px; +} + +@media (min-width: 768px) { + .text-title-4 { + font-size: var(--text-title-4-desktop); } } +/* Title/5: 21px, weight 500, line-height 1.1, tracking -0.42px */ +.text-title-5 { + font-family: var(--font-abc-diatype); + font-size: var(--text-title-5); + font-weight: 500; + line-height: 1.1; + letter-spacing: -0.42px; +} + +/* Body/XL: 19px, weight 450, line-height 1.3, tracking -0.19px */ +.text-body-xl { + font-family: var(--font-inter); + font-size: var(--text-body-xl); + font-weight: 450; + line-height: 1.3; + letter-spacing: -0.19px; +} + +/* Body/L: 16px, weight 450, line-height normal, tracking -0.16px */ +.text-body-l { + font-family: var(--font-inter); + font-size: var(--text-body-l); + font-weight: 450; + line-height: normal; + letter-spacing: -0.16px; +} + +/* Body/Md: 14px, weight 450, line-height normal, tracking -0.14px */ +.text-body-md { + font-family: var(--font-inter); + font-size: var(--text-body-md); + font-weight: 450; + line-height: normal; + letter-spacing: -0.14px; +} + +/* Nav Item: 14px, weight 550, line-height 18px, tracking -0.28px */ +.text-nav-item { + font-family: var(--font-inter); + font-size: var(--text-nav-item); + font-weight: 550; + line-height: 18px; + letter-spacing: -0.28px; +} + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + --font-abc-diatype: var(--font-abc-diatype), system-ui, sans-serif; + --font-berkeley-mono: var(--font-berkeley-mono), ui-monospace, monospace; + --font-inter: var(--font-inter), system-ui, sans-serif; +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); + --color-border-low: oklch(0.269 0 0); + --color-border-medium: oklch(0.3356 0.0046 39.42); + --diagonal-pattern-color: oklch(0.269 0 0 / 0.3); +} + @layer base { * { - border-color: hsl(var(--border)); + @apply border-border outline-ring/50; } - body { - background-color: hsl(var(--background)); - color: hsl(var(--foreground)); - font-family: var(--font-sans), sans-serif; - font-weight: 300; + @apply bg-background text-foreground; } +} + +/* Custom wide-spaced dashed borders using background gradients */ +.border-l-dashed-wide { + background: repeating-linear-gradient( + to bottom, + var(--color-border-low) 0px, + var(--color-border-low) 12px, + transparent 12px, + transparent 24px + ) + left/1px 100% no-repeat; +} + +.border-r-dashed-wide { + background: repeating-linear-gradient( + to bottom, + var(--color-border-low) 0px, + var(--color-border-low) 12px, + transparent 12px, + transparent 24px + ) + right/1px 100% no-repeat; +} + +.border-lr-dashed-wide { + background: + repeating-linear-gradient( + to bottom, + var(--color-border-low) 0px, + var(--color-border-low) 12px, + transparent 12px, + transparent 24px + ) + left/1px 100% no-repeat, + repeating-linear-gradient( + to bottom, + var(--color-border-low) 0px, + var(--color-border-low) 12px, + transparent 12px, + transparent 24px + ) + right/1px 100% no-repeat; +} + +.border-horizontal-dashed-wide { + background: + repeating-linear-gradient( + to right, + var(--color-border-low) 0px, + var(--color-border-low) 12px, + transparent 12px, + transparent 24px + ) + top/100% 1px no-repeat, + repeating-linear-gradient( + to right, + var(--color-border-low) 0px, + var(--color-border-low) 12px, + transparent 12px, + transparent 24px + ) + bottom/100% 1px no-repeat; +} + +.border-vertical-dashed-wide { + background: repeating-linear-gradient( + to bottom, + var(--color-border-low) 0px, + var(--color-border-low) 12px, + transparent 12px, + transparent 24px + ) + center/1px 100% no-repeat; +} + +/* Border medium dashed - 6px dash + 6px gap */ +.border-all-dashed-medium { + background: + repeating-linear-gradient( + to right, + var(--color-border-medium) 0px, + var(--color-border-medium) 6px, + transparent 6px, + transparent 12px + ) + top/100% 1px no-repeat, + repeating-linear-gradient( + to right, + var(--color-border-medium) 0px, + var(--color-border-medium) 6px, + transparent 6px, + transparent 12px + ) + bottom/100% 1px no-repeat, + repeating-linear-gradient( + to bottom, + var(--color-border-medium) 0px, + var(--color-border-medium) 6px, + transparent 6px, + transparent 12px + ) + left/1px 100% no-repeat, + repeating-linear-gradient( + to bottom, + var(--color-border-medium) 0px, + var(--color-border-medium) 6px, + transparent 6px, + transparent 12px + ) + right/1px 100% no-repeat; +} - /* Docs typography */ - .prose h1 { - @apply text-3xl md:text-4xl font-bold mb-4; +.border-lr-dashed-medium { + background: + repeating-linear-gradient( + to bottom, + var(--color-border-medium) 0px, + var(--color-border-medium) 6px, + transparent 6px, + transparent 12px + ) + left/1px 100% no-repeat, + repeating-linear-gradient( + to bottom, + var(--color-border-medium) 0px, + var(--color-border-medium) 6px, + transparent 6px, + transparent 12px + ) + right/1px 100% no-repeat; +} + +/* Product card diagonal background - fade in on hover using opacity */ +.product-card-diagonal { + position: relative; + background-image: repeating-linear-gradient( + 45deg, + transparent, + transparent 6px, + rgba(233, 230, 227, 0.5) 6px, + rgba(233, 230, 227, 0.5) 7px + ); + transition: background-image 0.2s ease-in-out; +} + +.product-card-diagonal::before { + content: ''; + position: absolute; + inset: 0; + background-image: repeating-linear-gradient( + 45deg, + transparent, + transparent 6px, + rgba(233, 230, 227, 1) 6px, + rgba(233, 230, 227, 1) 7px + ); + opacity: 0; + transition: opacity 180ms ease-in-out; + pointer-events: none; +} + +.group:hover .product-card-diagonal::before { + opacity: 1; +} + +/* ABC Diatype font utilities */ +.font-diatype { + font-family: var(--font-abc-diatype); +} + +.font-diatype-mono { + font-family: var(--font-berkeley-mono); +} + +.font-berkeley-mono { + font-family: var(--font-berkeley-mono); + font-weight: 400; +} + +.font-diatype-regular { + font-family: var(--font-abc-diatype); + font-weight: 400; +} + +.font-diatype-medium { + font-family: var(--font-abc-diatype); + font-weight: 500; +} + +.font-diatype-bold { + font-family: var(--font-abc-diatype); + font-weight: 700; +} + +/* Inter font utilities for body text */ +.font-inter { + font-family: var(--font-inter); +} + +.font-inter-regular { + font-family: var(--font-inter); + font-weight: 450; +} + +.font-inter-medium { + font-family: var(--font-inter); + font-weight: 550; +} + +.font-inter-semibold { + font-family: var(--font-inter); + font-weight: 600; +} + +/* Pulse animations for hero lines and borders */ +@keyframes pulse-border-top { + 0% { + left: -2px; + opacity: 1; + filter: blur(0px); } - .prose h2 { - @apply text-2xl font-semibold mt-8 mb-3; + 2% { + opacity: 1; + filter: blur(0px); } - .prose h3 { - @apply text-xl font-semibold mt-6 mb-2; + 98% { + opacity: 1; + filter: blur(0px); } - .prose p { - @apply leading-relaxed; + 100% { + left: 100%; + opacity: 1; + filter: blur(0px); } - .prose ul { - @apply list-disc pl-5; +} + +@keyframes pulse-border-right { + 0% { + top: -2px; + opacity: 1; + filter: blur(0px); + } + 2% { + opacity: 1; + filter: blur(0px); } + 98% { + opacity: 1; + filter: blur(0px); + } + 100% { + top: 100%; + opacity: 1; + filter: blur(0px); + } +} - input, - textarea, - select { - @apply px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:border-transparent; - background-color: hsl(var(--card)); - color: hsl(var(--foreground)); - --tw-ring-color: hsl(var(--ring)); - transition: - box-shadow 0.2s, - border-color 0.2s; +@keyframes pulse-border-bottom { + 0% { + right: -2px; + opacity: 1; + filter: blur(0px); } + 2% { + opacity: 1; + filter: blur(0px); + } + 98% { + opacity: 1; + filter: blur(0px); + } + 100% { + right: 100%; + opacity: 1; + filter: blur(0px); + } +} - input:disabled, - textarea:disabled, - select:disabled { - @apply cursor-not-allowed opacity-70; - background-color: hsl(var(--muted)); - color: hsl(var(--muted-foreground)); +@keyframes pulse-border-left { + 0% { + bottom: -2px; + opacity: 1; + filter: blur(0px); + } + 2% { + opacity: 1; + filter: blur(0px); } + 98% { + opacity: 1; + filter: blur(0px); + } + 100% { + bottom: 100%; + opacity: 1; + filter: blur(0px); + } +} - label { - @apply block text-sm font-medium mb-2; +@keyframes pulse-line-horizontal { + 0% { + left: 0; + opacity: 0; + filter: blur(1px); + } + 3% { + opacity: 0.6; + filter: blur(0.5px); + } + 10% { + opacity: 0.9; + filter: blur(0px); + } + 90% { + opacity: 0.9; + filter: blur(0px); } + 97% { + opacity: 0.6; + filter: blur(0.5px); + } + 100% { + left: 100%; + opacity: 0; + filter: blur(1px); + } +} - button { - @apply px-4 py-2 rounded-md font-medium focus:outline-none focus:ring-2; - --tw-ring-color: hsl(var(--ring)); - transition: - background 0.2s, - color 0.2s; +@keyframes pulse-line-vertical { + 0% { + top: 0; + opacity: 0; + filter: blur(1px); + } + 3% { + opacity: 0.6; + filter: blur(0.5px); + } + 10% { + opacity: 0.9; + filter: blur(0px); + } + 90% { + opacity: 0.9; + filter: blur(0px); + } + 97% { + opacity: 0.6; + filter: blur(0.5px); + } + 100% { + top: 100%; + opacity: 0; + filter: blur(1px); } } -.mosaic-text { - position: relative; - z-index: 1; - background-image: radial-gradient(circle, var(--mosaic-gradient-color) 55%, transparent 0%); - background-size: 2px 3px; - background-position: 0px 1px; - background-repeat: repeat; - background-clip: text; - color: transparent; +.pulse-border-top { + animation: pulse-border-top 4s ease-in-out infinite; + animation-delay: 0s; } -.mosaic-text::before { - content: ''; - position: absolute; - inset: -5%; - z-index: -1; - border-radius: 9999px; - background: - radial-gradient(circle at 30% 40%, #1fff9a 0%, transparent 70%), - radial-gradient(circle at 70% 60%, #9945ff 0%, transparent 70%); - filter: blur(20px); - opacity: 0.3; - pointer-events: none; +.pulse-border-right { + animation: pulse-border-right 4s ease-in-out infinite; + animation-delay: 1s; +} + +.pulse-border-bottom { + animation: pulse-border-bottom 4s ease-in-out infinite; + animation-delay: 2s; +} + +.pulse-border-left { + animation: pulse-border-left 4s ease-in-out infinite; + animation-delay: 3s; +} + +.pulse-line-horizontal-1 { + animation: pulse-line-horizontal 6s ease-in-out infinite; + animation-delay: 0.5s; +} + +.pulse-line-horizontal-2 { + animation: pulse-line-horizontal 6s ease-in-out infinite; + animation-delay: 3.5s; +} + +.pulse-line-vertical-1 { + animation: pulse-line-vertical 6s ease-in-out infinite; + animation-delay: 1.5s; +} + +.pulse-line-vertical-2 { + animation: pulse-line-vertical 6s ease-in-out infinite; + animation-delay: 4.5s; +} + +/* CTA Illustration Animations - Only float movement */ + +/* Float loop animations - up and down movement */ +@keyframes cta-float { + 0% { + transform: translateY(0); + } + 50% { + transform: translateY(-24px); + } + 100% { + transform: translateY(0); + } +} + +/* Apply animations to layers with staggered delays */ +.cta-layer-1 { + animation: cta-float 4s cubic-bezier(0.5, 0, 0, 1) 1.8s infinite; + transform-origin: center; +} + +.cta-layer-2 { + animation: cta-float 4s cubic-bezier(0.5, 0, 0, 1) 2.1s infinite; + transform-origin: center; +} + +.cta-layer-3 { + animation: cta-float 4s cubic-bezier(0.5, 0, 0, 1) 2.3s infinite; + transform-origin: center; +} + +/* Product Hero Entrance Animations */ +@keyframes hero-entrance { + 0% { + opacity: 0; + transform: scale(0.8) translateY(10px); + } + 100% { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +.hero-logo { + animation: hero-entrance 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) 0.1s both; +} + +.hero-inner-decoration { + animation: hero-entrance 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) 0.3s both; +} + +.hero-outer-decoration { + animation: hero-entrance 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) 0.5s both; +} + +.hero-title { + animation: hero-entrance 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) 0.6s both; +} + +.hero-description { + animation: hero-entrance 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) 0.7s both; +} + +.hero-actions { + animation: hero-entrance 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) 0.8s both; +} + +/* Logo Grid Illustration Animation */ +@keyframes illustration-entrance { + 0% { + opacity: 0; + transform: translateY(30px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +.illustration-animate { + animation: illustration-entrance 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) 0.2s both; } diff --git a/apps/app/src/app/layout.tsx b/apps/app/src/app/layout.tsx index f9723f7..a0afe18 100644 --- a/apps/app/src/app/layout.tsx +++ b/apps/app/src/app/layout.tsx @@ -1,24 +1,68 @@ import type { Metadata } from 'next'; -import { Inter, Fira_Mono } from 'next/font/google'; -// Import globals.css before any other components to avoid FOUC during HMR +import localFont from 'next/font/local'; import './globals.css'; -import { ThemeProvider } from '@/components/theme-provider'; import { Header } from '@/components/layout/header'; -import { Footer } from '@/components/layout/footer'; +// import { Footer } from '@/components/layout/footer'; import { cn } from '@/lib/utils'; -import { ChainContextProvider } from '@/context/ChainContextProvider'; -import { SelectedWalletAccountContextProvider } from '@/context/SelectedWalletAccountContextProvider'; -import { RpcContextProvider } from '@/context/RpcContextProvider'; +import { Providers } from './providers'; +import { Toaster } from '@/components/ui/sonner'; -const fontSans = Inter({ - subsets: ['latin'], - weight: ['300', '500', '700'], - variable: '--font-sans', +// Inter Variable font for body text with weights: 450, 550, 600 +const inter = localFont({ + src: '../fonts/InterVariable.woff2', + variable: '--font-inter', + display: 'swap', }); -const fontMono = Fira_Mono({ - subsets: ['latin'], - weight: ['400', '500', '700'], - variable: '--font-mono', + +// ABC Diatype fonts +const abcDiatype = localFont({ + src: [ + { + path: '../fonts/ABCDiatype-Regular.woff2', + weight: '400', + style: 'normal', + }, + { + path: '../fonts/ABCDiatype-Medium.woff2', + weight: '500', + style: 'normal', + }, + { + path: '../fonts/ABCDiatype-Bold.woff2', + weight: '700', + style: 'normal', + }, + ], + variable: '--font-abc-diatype', + display: 'swap', +}); + +// Berkeley Mono fonts +const berkeleyMono = localFont({ + src: [ + { + path: '../fonts/BerkeleyMono-Regular.otf', + weight: '400', + style: 'normal', + }, + { + path: '../fonts/BerkeleyMono-Oblique.otf', + weight: '400', + style: 'italic', + }, + { + path: '../fonts/BerkeleyMono-Bold.otf', + weight: '700', + style: 'normal', + }, + { + path: '../fonts/BerkeleyMono-Bold-Oblique.otf', + weight: '700', + style: 'italic', + }, + ], + variable: '--font-berkeley-mono', + display: 'swap', }); export const metadata: Metadata = { @@ -29,20 +73,15 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - - - - - -
-
-
{children}
-
-
-
-
-
-
+ + +
+
+
{children}
+ {/*
*/} +
+ +
); diff --git a/apps/app/src/app/manage/[address]/page.tsx b/apps/app/src/app/manage/[address]/page.tsx new file mode 100644 index 0000000..4f6e0f8 --- /dev/null +++ b/apps/app/src/app/manage/[address]/page.tsx @@ -0,0 +1,808 @@ +'use client'; + +import { useEffect, useState, useMemo, useRef } from 'react'; +import { Button } from '@/components/ui/button'; +import { + ChevronLeft, + ChevronDown, + Coins, + ArrowRightLeft, + Flame, + Ban, + Trash2, + Snowflake, + Sun, + Send, + FileText, + XCircle, +} from 'lucide-react'; +import Link from 'next/link'; +import { useParams, useRouter } from 'next/navigation'; +import { TokenDisplay } from '@/types/token'; +import { Spinner } from '@/components/ui/spinner'; +import { useConnector } from '@solana/connector/react'; +import { useTokenStore } from '@/stores/token-store'; +import { useTokenExtensionStore, usePauseState } from '@/stores/token-extension-store'; +import { TokenOverview } from '@/features/token-management/components/token-overview'; +import { TokenAuthorities } from '@/features/token-management/components/token-authorities'; +import { TokenExtensions } from '@/features/token-management/components/token-extensions'; +import { TransferRestrictions } from '@/features/token-management/components/transfer-restrictions'; +import { AddressModal } from '@/features/token-management/components/modals/address-modal'; +import { MintModalContent } from '@/features/token-management/components/modals/mint-modal-refactored'; +import { ForceTransferModalContent } from '@/features/token-management/components/modals/force-transfer-modal-refactored'; +import { ForceBurnModalContent } from '@/features/token-management/components/modals/force-burn-modal-refactored'; +import { ActionResultModal } from '@/features/token-management/components/modals/action-result-modal'; +import { PauseConfirmModalContent } from '@/features/token-management/components/modals/pause-confirm-modal'; +import { FreezeThawModalContent } from '@/features/token-management/components/modals/freeze-thaw-modal'; +import { TransferModalContent } from '@/features/token-management/components/modals/transfer-modal'; +import { BurnModalContent } from '@/features/token-management/components/modals/burn-modal'; +import { UpdateMetadataModalContent } from '@/features/token-management/components/modals/update-metadata-modal'; +import { CloseAccountModalContent } from '@/features/token-management/components/modals/close-account-modal'; +import { DeleteTokenModalContent } from '@/features/dashboard/components/delete-token-modal'; +import { useConnectorSigner } from '@/features/wallet/hooks/use-connector-signer'; +import { + addAddressToBlocklist, + addAddressToAllowlist, + removeAddressFromBlocklist, + removeAddressFromAllowlist, +} from '@/features/token-management/lib/access-list'; +import { Address, createSolanaRpc, Rpc, SolanaRpcApi } from '@solana/kit'; +import { getList, getListConfigPda, getTokenExtensions } from '@mosaic/sdk'; +import { Mode } from '@token-acl/abl-sdk'; +import { buildAddressExplorerUrl } from '@/lib/solana/explorer'; +import { getTokenAuthorities } from '@/lib/solana/rpc'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { AlertDialog, AlertDialogTrigger } from '@/components/ui/alert-dialog'; +import { IconArrowUpRight, IconHexagonFill } from 'symbols-react'; + +export default function ManageTokenPage() { + const { connected, selectedAccount } = useConnector(); + const params = useParams(); + const address = params.address as string; + + if (!connected || !selectedAccount) { + return ( +
+
+

Wallet Required

+

Please connect your Solana wallet to manage tokens.

+
+
+ ); + } + + return ; +} + +const getAccessList = async ( + rpc: Rpc, + authority: Address, + mint: Address, +): Promise<{ type: 'allowlist' | 'blocklist'; wallets: string[] } | null> => { + try { + const listConfigPda = await getListConfigPda({ + authority, + mint, + }); + const list = await getList({ rpc, listConfig: listConfigPda }); + return { + type: list.mode === Mode.Allow ? 'allowlist' : 'blocklist', + wallets: list.wallets, + }; + } catch { + // List config account doesn't exist yet - this is normal for tokens that + // have SRFC-37 enabled but haven't had their access list initialized + return null; + } +}; + +function ManageTokenConnected({ address }: { address: string }) { + const router = useRouter(); + const { selectedAccount, cluster } = useConnector(); + const findTokenByAddress = useTokenStore(state => state.findTokenByAddress); + const removeToken = useTokenStore(state => state.removeToken); + const updateToken = useTokenStore(state => state.updateToken); + const [token, setToken] = useState(null); + const [loading, setLoading] = useState(true); + const [accessList, setAccessList] = useState([]); + const [listType, setListType] = useState<'allowlist' | 'blocklist'>('blocklist'); + const [newAddress, setNewAddress] = useState(''); + const [showAccessListModal, setShowAccessListModal] = useState(false); + const [actionInProgress, setActionInProgress] = useState(false); + const [error, setError] = useState(''); + const [transactionSignature, setTransactionSignature] = useState(''); + const [refreshTrigger, setRefreshTrigger] = useState(0); + const [supplyRefreshTrigger, setSupplyRefreshTrigger] = useState(0); + + // Use centralized extension store for pause state + const { isPaused, isUpdating: isPauseUpdating, error: pauseError } = usePauseState(address); + const { fetchPauseState, togglePause, updateExtensionField } = useTokenExtensionStore(); + + // Function to trigger supply refresh after mint/burn actions + const refreshSupply = () => { + setSupplyRefreshTrigger(prev => prev + 1); + }; + + const rpc = useMemo(() => { + if (!cluster?.url) return null; + return createSolanaRpc(cluster.url) as Rpc; + }, [cluster?.url]); + + const loadedAccessListRef = useRef(null); + + const refreshAccessList = () => { + setTimeout(() => { + loadedAccessListRef.current = null; + setRefreshTrigger(prev => prev + 1); + }, 600); + }; + + // Use the connector signer hook which provides a gill-compatible transaction signer + const transactionSendingSigner = useConnectorSigner(); + + useEffect(() => { + const addTokenExtensionsToFoundToken = async (foundToken: TokenDisplay): Promise => { + if (!rpc) return; + + try { + const extensions = await getTokenExtensions(rpc, foundToken.address as Address); + foundToken.extensions = extensions; + + // Fetch authority information from the blockchain + try { + const rpcUrl = + cluster?.url ?? process.env.NEXT_PUBLIC_SOLANA_RPC_URL ?? 'https://api.devnet.solana.com'; + const authorities = await getTokenAuthorities(foundToken.address as Address, rpcUrl); + // Merge fetched authorities into the token, preserving existing values if they exist + foundToken.mintAuthority = authorities.mintAuthority || foundToken.mintAuthority; + foundToken.freezeAuthority = authorities.freezeAuthority || foundToken.freezeAuthority; + foundToken.metadataAuthority = authorities.metadataAuthority || foundToken.metadataAuthority; + foundToken.pausableAuthority = authorities.pausableAuthority || foundToken.pausableAuthority; + foundToken.confidentialBalancesAuthority = + authorities.confidentialBalancesAuthority || foundToken.confidentialBalancesAuthority; + foundToken.permanentDelegateAuthority = + authorities.permanentDelegateAuthority || foundToken.permanentDelegateAuthority; + foundToken.scaledUiAmountAuthority = + authorities.scaledUiAmountAuthority || foundToken.scaledUiAmountAuthority; + } catch { + // If authority fetch fails, continue with existing token data + // Authorities may not be available if token doesn't exist on this network + } + + setToken(foundToken); + + // Fetch pause state using centralized store + if (foundToken.address) { + fetchPauseState(foundToken.address, cluster?.url || ''); + } + } catch { + // Token might not exist on this network - show the token with empty extensions + setToken(foundToken); + } + }; + + const loadTokenData = () => { + const foundToken = findTokenByAddress(address); + + if (foundToken) { + setToken(foundToken); + addTokenExtensionsToFoundToken(foundToken); + } + + setLoading(false); + }; + + loadTokenData(); + }, [address, rpc, cluster?.url, findTokenByAddress, fetchPauseState]); + + useEffect(() => { + const loadAccessList = async () => { + if (!rpc) return; + + const currentKey = `${selectedAccount}-${token?.address}-${cluster?.url}-${refreshTrigger}`; + + if (loadedAccessListRef.current === currentKey) { + return; + } + + const result = await getAccessList(rpc, selectedAccount as Address, token?.address as Address); + if (result) { + setAccessList(result.wallets); + setListType(result.type); + } else { + // Access list not initialized yet - set empty list + setAccessList([]); + } + loadedAccessListRef.current = currentKey; + }; + + if (rpc && selectedAccount && token?.address && token?.isSrfc37) { + loadAccessList(); + } + }, [rpc, selectedAccount, token?.address, token?.isSrfc37, cluster?.url, refreshTrigger]); + + const openInExplorer = () => { + window.open(buildAddressExplorerUrl(address, cluster), '_blank'); + }; + + const addToAccessList = async () => { + setShowAccessListModal(false); + if (newAddress.trim() && accessList.includes(newAddress.trim())) { + setError('Address already in list'); + return; + } + + await handleAddToAccessList(token?.address || '', newAddress.trim()); + setNewAddress(''); + }; + + const removeFromAccessList = async (address: string) => { + if (!selectedAccount || !token?.address || !transactionSendingSigner || !rpc) { + setError('Required parameters not available'); + return; + } + + setActionInProgress(true); + setError(''); + + try { + let result; + if (listType === 'blocklist') { + result = await removeAddressFromBlocklist( + rpc, + { + mintAddress: token.address, + walletAddress: address, + }, + transactionSendingSigner, + cluster?.url || '', + ); + } else { + result = await removeAddressFromAllowlist( + rpc, + { + mintAddress: token.address, + walletAddress: address, + }, + transactionSendingSigner, + cluster?.url || '', + ); + } + + if (result.success) { + setTransactionSignature(result.transactionSignature || ''); + refreshAccessList(); + } else { + setError(result.error || 'Removal failed'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setActionInProgress(false); + } + }; + + const handleAddToAccessList = async (mintAddress: string, address: string) => { + if (!selectedAccount || !rpc) { + setError('Wallet not connected or RPC not available'); + return; + } + + setActionInProgress(true); + setError(''); + + try { + if (!transactionSendingSigner) { + throw new Error('Transaction signer not available'); + } + + let result; + if (listType === 'blocklist') { + result = await addAddressToBlocklist( + rpc, + { + mintAddress, + walletAddress: address, + }, + transactionSendingSigner, + cluster?.url || '', + ); + } else { + result = await addAddressToAllowlist( + rpc, + { + mintAddress, + walletAddress: address, + }, + transactionSendingSigner, + cluster?.url || '', + ); + } + + if (result.success) { + setTransactionSignature(result.transactionSignature || ''); + refreshAccessList(); + } else { + setError(result.error || 'Operation failed'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setActionInProgress(false); + } + }; + + const handleRemoveFromStorage = () => { + removeToken(address); + router.push('/'); + }; + + const handlePauseConfirm = async () => { + if (!selectedAccount || !token?.address || !transactionSendingSigner) { + updateExtensionField(address, 'pause', { error: 'Required parameters not available' }); + return; + } + + // Check if the connected wallet has pause authority + const walletAddress = String(selectedAccount); + const pauseAuthority = token.pausableAuthority ? String(token.pausableAuthority) : ''; + + if (pauseAuthority && pauseAuthority !== walletAddress) { + updateExtensionField(address, 'pause', { + error: 'Connected wallet does not have pause authority. Only the pause authority can pause/unpause this token.', + }); + return; + } + + // Use centralized store to toggle pause + await togglePause( + token.address, + { + pauseAuthority: token.pausableAuthority, + feePayer: selectedAccount, + rpcUrl: cluster?.url || '', + }, + transactionSendingSigner, + ); + }; + + if (loading) { + return ( +
+
+
+ +

Loading token details...

+
+
+
+ ); + } + + if (!token) { + return ( +
+
+
+

Token Not Found

+

+ The token with address {address} could not be found in your local storage. +

+ + + +
+
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+
+
+ + + +
+ {token.image ? ( + {token.name + ) : ( + + )} +
+
+

{token.name}

+

{token.symbol}

+
+
+ +
+ + + + + + + + + Token Actions + + + {transactionSendingSigner && ( + <> + {token?.mintAuthority && ( + + + e.preventDefault()} + > + + Mint Tokens + + + + + )} + + + e.preventDefault()} + > + + Transfer Tokens + + + + + + + e.preventDefault()} + > + + Burn Tokens + + + + + {token?.metadataAuthority && ( + + + e.preventDefault()} + > + + Update Metadata + + + { + updateToken(address, { + ...(updates.name && { name: updates.name }), + ...(updates.symbol && { symbol: updates.symbol }), + ...(updates.uri && { metadataUri: updates.uri }), + }); + // Also update local state for immediate UI feedback + setToken(prev => + prev + ? { + ...prev, + ...(updates.name && { name: updates.name }), + ...(updates.symbol && { + symbol: updates.symbol, + }), + ...(updates.uri && { + metadataUri: updates.uri, + }), + } + : prev, + ); + }} + /> + + )} + {token?.permanentDelegateAuthority && ( + <> + + + Administrative Actions + + + + + + e.preventDefault()} + > + + Force Transfer + + + + + + + e.preventDefault()} + > + + Force Burn + + + + + + )} + {token?.freezeAuthority && ( + <> + + + e.preventDefault()} + > + + Freeze Account + + + + + + + e.preventDefault()} + > + + Thaw Account + + + + + + )} + + + e.preventDefault()} + > + + Close Token Account + + + + + + )} + {token?.pausableAuthority && ( + <> + { + if (!open) { + updateExtensionField(address, 'pause', { error: null }); + } + }} + > + + e.preventDefault()} + > + {isPaused ? ( + <> + {' '} + Unpause Token + + ) : ( + <> + Pause + Token + + )} + + + + + + )} + + + + e.preventDefault()} + className="cursor-pointer text-red-600 hover:!text-red-600 hover:!bg-red-50 dark:hover:!text-red-600 dark:hover:!bg-red-800/40 rounded-lg" + > + + Remove from Storage + + + + + + +
+
+
+ + {/* Token Overview */} +
+

Token Overview

+ +
+ + {/* Settings */} +
+

Settings

+ +
+ + + Permissions + + {token.isSrfc37 && ( + + {listType === 'allowlist' ? 'Allowlist' : 'Blocklist'} + + )} + + Extensions + + +
+ +
+ + + + {token.isSrfc37 && ( + + setShowAccessListModal(true)} + onRemoveFromAccessList={removeFromAccessList} + /> + + )} + + + +
+
+
+
+ + {/* Modals - ActionResultModal and AddressModal remain controlled (not user-triggered) */} + { + setError(''); + setTransactionSignature(''); + setActionInProgress(false); + }} + actionInProgress={actionInProgress} + error={error} + transactionSignature={transactionSignature} + /> + + { + setShowAccessListModal(false); + setNewAddress(''); + }} + onAdd={addToAccessList} + newAddress={newAddress} + onAddressChange={setNewAddress} + title={`Add to ${listType === 'allowlist' ? 'Allowlist' : 'Blocklist'}`} + placeholder="Enter Solana address..." + buttonText={`Add to ${listType === 'allowlist' ? 'Allowlist' : 'Blocklist'}`} + /> +
+ ); +} diff --git a/apps/app/src/app/page.tsx b/apps/app/src/app/page.tsx index 7b806ad..fd30490 100644 --- a/apps/app/src/app/page.tsx +++ b/apps/app/src/app/page.tsx @@ -1,9 +1,41 @@ -import { Hero } from '@/components/sections/hero'; - -export default function HomePage() { - return ( -
- -
- ); +'use client'; + +import { useEffect, useState } from 'react'; +import { useConnector } from '@solana/connector/react'; +import { Spinner } from '@/components/ui/spinner'; +import { DashboardConnected } from '@/features/dashboard/components/dashboard-connected'; +import { DashboardDisconnected } from '@/features/dashboard/components/dashboard-disconnected'; + +export default function DashboardPage() { + const { connected, selectedAccount, connecting } = useConnector(); + const [isInitializing, setIsInitializing] = useState(true); + + useEffect(() => { + if (connected || connecting) { + setIsInitializing(false); + return; + } + + const recentWallet = localStorage.getItem('recentlyConnectedWallet'); + if (!recentWallet) { + setIsInitializing(false); + return; + } + + const timer = setTimeout(() => { + setIsInitializing(false); + }, 1000); + + return () => clearTimeout(timer); + }, [connected, connecting]); + + if (connecting || (isInitializing && !connected)) { + return ( +
+ +
+ ); + } + + return connected && selectedAccount ? : ; } diff --git a/apps/app/src/app/providers.tsx b/apps/app/src/app/providers.tsx new file mode 100644 index 0000000..e20767e --- /dev/null +++ b/apps/app/src/app/providers.tsx @@ -0,0 +1,76 @@ +'use client'; + +import { useMemo, type ReactNode } from 'react'; +import { AppProvider } from '@solana/connector/react'; +import { getDefaultConfig, getDefaultMobileConfig } from '@solana/connector/headless'; +import { ThemeProvider } from '@/components/theme-provider'; +import { useRpcStore } from '@/stores/rpc-store'; + +export function Providers({ children }: { children: ReactNode }) { + const customRpcs = useRpcStore(state => state.customRpcs); + + const connectorConfig = useMemo(() => { + // Get custom RPC URL from environment variable + const envRpcUrl = process.env.NEXT_PUBLIC_SOLANA_RPC_URL; + + // Base clusters - always available + const baseClusters = [ + { + id: 'solana:mainnet' as const, + label: envRpcUrl ? 'Mainnet (Env RPC)' : 'Mainnet', + name: 'mainnet-beta' as const, + url: envRpcUrl || 'https://api.mainnet-beta.solana.com', + }, + { + id: 'solana:devnet' as const, + label: 'Devnet', + name: 'devnet' as const, + url: 'https://api.devnet.solana.com', + }, + { + id: 'solana:testnet' as const, + label: 'Testnet', + name: 'testnet' as const, + url: 'https://api.testnet.solana.com', + }, + ]; + + // Add user-defined custom RPCs + const userClusters = customRpcs.map(rpc => ({ + id: rpc.id as `solana:${string}`, + label: rpc.label, + name: rpc.network, + url: rpc.url, + })); + + const clusters = [...baseClusters, ...userClusters]; + + return getDefaultConfig({ + appName: 'Mosaic - Tokenization Engine', + appUrl: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000', + autoConnect: true, + enableMobile: true, + clusters, + }); + }, [customRpcs]); + + const mobile = useMemo( + () => + getDefaultMobileConfig({ + appName: 'Mosaic - Tokenization Engine', + appUrl: + process.env.NEXT_PUBLIC_MOBILE_APP_URL || + process.env.NEXT_PUBLIC_APP_URL || + 'http://localhost:3000', + }), + [], + ); + + return ( + + + {children} + + + ); +} diff --git a/apps/app/src/components/.gitkeep b/apps/app/src/components/.gitkeep deleted file mode 100644 index 36abe9c..0000000 --- a/apps/app/src/components/.gitkeep +++ /dev/null @@ -1,4 +0,0 @@ -# React components will be implemented here -# - ui/ -# - forms/ -# - layout/ \ No newline at end of file diff --git a/apps/app/src/components/ConnectWallet/ConnectWalletMenu.tsx b/apps/app/src/components/ConnectWallet/ConnectWalletMenu.tsx deleted file mode 100644 index f464800..0000000 --- a/apps/app/src/components/ConnectWallet/ConnectWalletMenu.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { StandardConnect, StandardDisconnect } from '@wallet-standard/core'; -import { - type UiWallet, - type UiWalletAccount, - uiWalletAccountBelongsToUiWallet, - useWallets, -} from '@wallet-standard/react'; -import { useContext, useState } from 'react'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; -import { Button } from '@/components/ui/button'; -import { ChevronDown, Wallet } from 'lucide-react'; - -import { SelectedWalletAccountContext } from '@/context/SelectedWalletAccountContext'; -import { ConnectWalletMenuItem } from './ConnectWalletMenuItem'; -import { UnconnectableWalletMenuItem } from './UnconnectableWalletMenuItem'; -import { WalletAccountIcon } from './WalletAccountIcon'; - -type Props = Readonly<{ - children: React.ReactNode; -}>; - -type WalletMenuItemProps = { - wallet: UiWallet; - error: unknown; - onAccountSelect: (account: UiWalletAccount) => void; - onDisconnect: (wallet: UiWallet) => void; - onError: (error: unknown) => void; -}; - -function WalletMenuItem({ wallet, error, onAccountSelect, onDisconnect, onError }: WalletMenuItemProps) { - if (error) { - return ; - } - return ( - - ); -} - -export function ConnectWalletMenu({ children }: Props) { - const wallets = useWallets(); - const [selectedWalletAccount, setSelectedWalletAccount] = useContext(SelectedWalletAccountContext); - const [forceClose, setForceClose] = useState(false); - const [error, setError] = useState(); - const walletsThatSupportStandardConnect = []; - const unconnectableWallets = []; - for (const wallet of wallets) { - if ( - wallet.features.includes(StandardConnect) && - wallet.features.includes(StandardDisconnect) && - wallet.chains.some(chain => chain.includes('solana')) - ) { - walletsThatSupportStandardConnect.push(wallet); - } else { - unconnectableWallets.push(wallet); - } - } - return ( - <> - setForceClose(false)}> - - - - - {wallets.length === 0 ? ( -
- -

No wallets found

-

Install a Solana wallet to get started

-
- ) : ( -
- {!selectedWalletAccount && ( -
- Available Wallets -
- )} - {walletsThatSupportStandardConnect.map((wallet, index) => ( - { - setSelectedWalletAccount(account); - setForceClose(true); - }} - onDisconnect={wallet => { - if ( - selectedWalletAccount && - uiWalletAccountBelongsToUiWallet(selectedWalletAccount, wallet) - ) { - setSelectedWalletAccount(undefined); - } - }} - onError={setError} - /> - ))} -
- )} -
-
- - ); -} diff --git a/apps/app/src/components/ConnectWallet/ConnectWalletMenuItem.tsx b/apps/app/src/components/ConnectWallet/ConnectWalletMenuItem.tsx deleted file mode 100644 index 6ba1ba2..0000000 --- a/apps/app/src/components/ConnectWallet/ConnectWalletMenuItem.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { - type UiWallet, - type UiWalletAccount, - uiWalletAccountsAreSame, - useConnect, - useDisconnect, -} from '@wallet-standard/react'; -import { useCallback, useContext } from 'react'; - -import { SelectedWalletAccountContext } from '@/context/SelectedWalletAccountContext'; -import { WalletMenuItemContent } from './WalletMenuItemContent'; -import { - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, -} from '@/components/ui/dropdown-menu'; -import { Plus, LogOut, User } from 'lucide-react'; - -type Props = Readonly<{ - onAccountSelect(account: UiWalletAccount | undefined): void; - onDisconnect(wallet: UiWallet): void; - onError(err: unknown): void; - wallet: UiWallet; -}>; - -export function ConnectWalletMenuItem({ onAccountSelect, onDisconnect, onError, wallet }: Props) { - const [isConnecting, connect] = useConnect(wallet); - const [isDisconnecting, disconnect] = useDisconnect(wallet); - const isPending = isConnecting || isDisconnecting; - const isConnected = wallet.accounts.length > 0; - const [selectedWalletAccount] = useContext(SelectedWalletAccountContext); - const handleConnectClick = useCallback(async () => { - try { - const existingAccounts = [...wallet.accounts]; - const nextAccounts = await connect(); - // Try to choose the first never-before-seen account. - for (const nextAccount of nextAccounts) { - if (!existingAccounts.some(existingAccount => uiWalletAccountsAreSame(nextAccount, existingAccount))) { - onAccountSelect(nextAccount); - return; - } - } - // Failing that, choose the first account in the list. - if (nextAccounts[0]) { - onAccountSelect(nextAccounts[0]); - } - } catch (e) { - onError(e); - } - }, [connect, onAccountSelect, onError, wallet.accounts]); - return ( - - - - - - - - Accounts ({wallet.accounts.length}) - - - {wallet.accounts.map(account => ( - { - onAccountSelect(account); - }} - > - - {account.address.slice(0, 8)}...{account.address.slice(-4)} - - - ))} - - - { - e.preventDefault(); - await handleConnectClick(); - }} - > - - Connect More - - { - e.preventDefault(); - try { - await disconnect(); - onDisconnect(wallet); - } catch (e) { - onError(e); - } - }} - > - - Disconnect - - - - ); -} diff --git a/apps/app/src/components/ConnectWallet/UnconnectableWalletMenuItem.tsx b/apps/app/src/components/ConnectWallet/UnconnectableWalletMenuItem.tsx deleted file mode 100644 index 9d08266..0000000 --- a/apps/app/src/components/ConnectWallet/UnconnectableWalletMenuItem.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { UiWallet } from '@wallet-standard/react'; -import { useState } from 'react'; - -import { WalletMenuItemContent } from './WalletMenuItemContent'; -import { DropdownMenuItem } from '@/components/ui/dropdown-menu'; -import { AlertTriangleIcon } from 'lucide-react'; -import { Button } from '../ui/button'; - -type Props = Readonly<{ - error: unknown; - wallet: UiWallet; -}>; - -export function UnconnectableWalletMenuItem({ error, wallet }: Props) { - const [dialogIsOpen, setDialogIsOpen] = useState(false); - return ( - <> - setDialogIsOpen(true)}> - -
{wallet.name}
-
-
- -
-
- {dialogIsOpen ? ( -
-

Unconnectable wallet

-

{error instanceof Error ? error.message : 'Unknown error'}

- -
- ) : null} - - ); -} diff --git a/apps/app/src/components/ConnectWallet/WalletAccountIcon.tsx b/apps/app/src/components/ConnectWallet/WalletAccountIcon.tsx deleted file mode 100644 index c74bad1..0000000 --- a/apps/app/src/components/ConnectWallet/WalletAccountIcon.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { type UiWalletAccount, uiWalletAccountBelongsToUiWallet, useWallets } from '@wallet-standard/react'; -import Image from 'next/image'; -import React from 'react'; - -type Props = React.ComponentProps<'img'> & - Readonly<{ - account: UiWalletAccount; - }>; - -export function WalletAccountIcon({ account }: Props) { - const wallets = useWallets(); - let icon; - if (account.icon) { - icon = account.icon; - } else { - for (const wallet of wallets) { - if (uiWalletAccountBelongsToUiWallet(account, wallet)) { - icon = wallet.icon; - break; - } - } - } - return icon ? {account.address} : null; -} diff --git a/apps/app/src/components/ConnectWallet/WalletMenuItemContent.tsx b/apps/app/src/components/ConnectWallet/WalletMenuItemContent.tsx deleted file mode 100644 index c1460da..0000000 --- a/apps/app/src/components/ConnectWallet/WalletMenuItemContent.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import type { UiWallet } from '@wallet-standard/react'; -import { Loader2Icon } from 'lucide-react'; -import Image from 'next/image'; - -type Props = Readonly<{ - children?: React.ReactNode; - loading?: boolean; - wallet: UiWallet; -}>; - -export function WalletMenuItemContent({ children, loading, wallet }: Props) { - if (loading) { - return ( -
-
- -
- Connecting... -
- ); - } - - return ( -
-
- {wallet.name} -
-
- {children ?? wallet.name} -
-
- ); -} diff --git a/apps/app/src/components/CopyableExplorerField.tsx b/apps/app/src/components/CopyableExplorerField.tsx deleted file mode 100644 index 2bcf35e..0000000 --- a/apps/app/src/components/CopyableExplorerField.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { ExternalLink } from 'lucide-react'; - -interface CopyableExplorerFieldProps { - label: string; - value?: string; - kind: 'address' | 'tx'; - cluster?: 'devnet' | 'testnet' | 'mainnet-beta'; -} - -export function CopyableExplorerField({ label, value, kind, cluster = 'devnet' }: CopyableExplorerFieldProps) { - const [copied, setCopied] = useState(false); - const explorerPath = kind === 'address' ? 'address' : 'tx'; - - const onCopy = () => { - if (!value) return; - navigator.clipboard.writeText(value); - setCopied(true); - window.setTimeout(() => setCopied(false), 1500); - }; - - return ( -
- {label}: -
-
- - {value} - -
- {copied && Copied} - {value && ( - - )} -
-
- ); -} diff --git a/apps/app/src/components/CreateTemplateSidebar.tsx b/apps/app/src/components/CreateTemplateSidebar.tsx deleted file mode 100644 index e1cac85..0000000 --- a/apps/app/src/components/CreateTemplateSidebar.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { CardHeader, CardContent, CardTitle } from '@/components/ui/card'; -import { ReactNode } from 'react'; -import { CapabilityKey, ExtensionKey, capabilityNodes, extensionNodes } from '@/components/capabilities/registry'; - -interface CreateTemplateSidebarProps { - aboutTitle?: string; - description: ReactNode; - coreCapabilities?: ReactNode[]; - enabledExtensions?: ReactNode[]; - standards?: ReactNode[]; - coreCapabilityKeys?: CapabilityKey[]; - enabledExtensionKeys?: ExtensionKey[]; - standardKeys?: CapabilityKey[]; -} - -export function CreateTemplateSidebar({ - aboutTitle = 'About this template', - description, - coreCapabilities, - enabledExtensions, - standards, - coreCapabilityKeys, - enabledExtensionKeys, - standardKeys, -}: CreateTemplateSidebarProps) { - const resolvedCore = coreCapabilities ? coreCapabilities : (coreCapabilityKeys || []).map(k => capabilityNodes[k]); - const resolvedExtensions = enabledExtensions - ? enabledExtensions - : (enabledExtensionKeys || []).map(k => extensionNodes[k]); - const resolvedStandards = standards ? standards : (standardKeys || []).map(k => capabilityNodes[k]); - return ( -
- - {aboutTitle} - - -
{description}
- - {resolvedCore?.length > 0 && ( -
-

Core capabilities

-
    - {resolvedCore.map((item, idx) => ( -
  • {item}
  • - ))} -
-
- )} - - {(resolvedExtensions.length > 0 || resolvedStandards.length > 0) && ( -
-

Underlying standards

-
    - {resolvedExtensions.length > 0 && ( -
  • - Token extensions:{' '} - {resolvedExtensions.map((n, i) => ( - - {n} - {i < resolvedExtensions.length - 1 ? ', ' : ''} - - ))} -
  • - )} - {resolvedStandards.map((item, idx) => ( -
  • {item}
  • - ))} -
-
- )} - -
- You can manage lists and authorities later from the token management dashboard. -
-
-
- ); -} diff --git a/apps/app/src/components/copyable-explorer-field.tsx b/apps/app/src/components/copyable-explorer-field.tsx new file mode 100644 index 0000000..1b15907 --- /dev/null +++ b/apps/app/src/components/copyable-explorer-field.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { ExternalLink } from 'lucide-react'; +import { CopyButton } from '@/components/ui/copy-button'; +import { buildAddressExplorerUrl, buildExplorerUrl } from '@/lib/solana/explorer'; + +interface CopyableExplorerFieldProps { + label: string; + value?: string; + kind: 'address' | 'tx'; + cluster?: 'devnet' | 'testnet' | 'mainnet-beta'; +} + +export function CopyableExplorerField({ label, value, kind, cluster }: CopyableExplorerFieldProps) { + if (!value) { + return ( +
+ {label}: +
No value
+
+ ); + } + + const explorerUrl = + kind === 'tx' ? buildExplorerUrl(value, cluster) : buildAddressExplorerUrl(value, { name: cluster }); + + return ( +
+ {label}: +
+
+ + {value} + +
+ + +
+
+ ); +} diff --git a/apps/app/src/components/layout/footer.tsx b/apps/app/src/components/layout/footer.tsx index 8ed0899..f240772 100644 --- a/apps/app/src/components/layout/footer.tsx +++ b/apps/app/src/components/layout/footer.tsx @@ -3,8 +3,8 @@ import Image from 'next/image'; export function Footer() { return ( -