diff --git a/.gitignore b/.gitignore index 023a0bd12..27c2b6a62 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,3 @@ release-artifacts *.tgz .claude -apps/appkit-minter/src/pages/example.tsx diff --git a/apps/appkit-minter/src/core/configs/app-kit.ts b/apps/appkit-minter/src/core/configs/app-kit.ts index 2d39192d1..e16449cf1 100644 --- a/apps/appkit-minter/src/core/configs/app-kit.ts +++ b/apps/appkit-minter/src/core/configs/app-kit.ts @@ -6,9 +6,8 @@ * */ -import { AppKit, Network } from '@ton/appkit'; +import { AppKit, Network, createTonConnectConnector, ApiClientTonApi } from '@ton/appkit'; import { OmnistonSwapProvider } from '@ton/appkit/swap/omniston'; -import { TonConnectConnector, ApiClientTonApi } from '@ton/appkit'; import { ENV_TON_API_KEY_TESTNET, ENV_TON_API_KEY_MAINNET } from '@/core/configs/env'; @@ -34,7 +33,7 @@ export const appKit = new AppKit({ }, }, connectors: [ - new TonConnectConnector({ + createTonConnectConnector({ tonConnectOptions: { manifestUrl: 'https://tonconnect-sdk-demo-dapp.vercel.app/tonconnect-manifest.json', }, diff --git a/apps/appkit-next/.gitignore b/apps/appkit-next/.gitignore new file mode 100644 index 000000000..5ef6a5207 --- /dev/null +++ b/apps/appkit-next/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/appkit-next/README.md b/apps/appkit-next/README.md new file mode 100644 index 000000000..66bb426ff --- /dev/null +++ b/apps/appkit-next/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/apps/appkit-next/next.config.js b/apps/appkit-next/next.config.js new file mode 100644 index 000000000..134ac014d --- /dev/null +++ b/apps/appkit-next/next.config.js @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + distDir: 'dist', +}; + +export default nextConfig; diff --git a/apps/appkit-next/package.json b/apps/appkit-next/package.json new file mode 100644 index 000000000..8e5a8756d --- /dev/null +++ b/apps/appkit-next/package.json @@ -0,0 +1,32 @@ +{ + "name": "appkit-next", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "clean": "rm -rf .next dist .turbo node_modules" + }, + "dependencies": { + "@tanstack/react-query": "catalog:", + "@ton/appkit": "workspace:*", + "@ton/appkit-react": "workspace:*", + "@ton/core": "catalog:", + "@ton/crypto": "catalog:", + "@ton/walletkit": "workspace:*", + "@tonconnect/sdk": "catalog:", + "@tonconnect/ui": "catalog:", + "next": "16.1.6", + "react": "catalog:", + "react-dom": "catalog:" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^25.2.3", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "tailwindcss": "^4", + "typescript": "^5.9.3" + } +} diff --git a/apps/appkit-next/postcss.config.mjs b/apps/appkit-next/postcss.config.mjs new file mode 100644 index 000000000..f3926c273 --- /dev/null +++ b/apps/appkit-next/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; + +export default config; diff --git a/apps/appkit-next/public/ton.png b/apps/appkit-next/public/ton.png new file mode 100644 index 000000000..7bdf41f11 Binary files /dev/null and b/apps/appkit-next/public/ton.png differ diff --git a/apps/appkit-next/src/app/icon.png b/apps/appkit-next/src/app/icon.png new file mode 100644 index 000000000..f1f0faac2 Binary files /dev/null and b/apps/appkit-next/src/app/icon.png differ diff --git a/apps/appkit-next/src/app/layout.tsx b/apps/appkit-next/src/app/layout.tsx new file mode 100644 index 000000000..a6460489a --- /dev/null +++ b/apps/appkit-next/src/app/layout.tsx @@ -0,0 +1,31 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { Metadata } from 'next'; +import type { ReactNode } from 'react'; + +import AppKitContext from '@/core/contexts/context'; + +import '@/core/styles/app.css'; +import '@/core/styles/index.css'; +import '@ton/appkit-react/styles.css'; + +export const metadata: Metadata = { + title: 'NFT Minter - AppKit Demo App', + description: 'NFT Minter - AppKit Demo App', +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/apps/appkit-next/src/app/page.tsx b/apps/appkit-next/src/app/page.tsx new file mode 100644 index 000000000..038aa65ab --- /dev/null +++ b/apps/appkit-next/src/app/page.tsx @@ -0,0 +1,13 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { MinterPage } from '@/core/pages/minter-page'; + +export default function Home() { + return ; +} diff --git a/apps/appkit-next/src/core/components/common/button.tsx b/apps/appkit-next/src/core/components/common/button.tsx new file mode 100644 index 000000000..b30647602 --- /dev/null +++ b/apps/appkit-next/src/core/components/common/button.tsx @@ -0,0 +1,62 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type React from 'react'; +import { Loader2 } from 'lucide-react'; + +import { cn } from '@/core/lib/utils'; + +interface ButtonProps extends React.ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'danger' | 'ghost'; + size?: 'sm' | 'md' | 'lg'; + isLoading?: boolean; + children: React.ReactNode; +} + +export const Button: React.FC = ({ + variant = 'primary', + size = 'md', + isLoading = false, + children, + disabled, + className = '', + ...props +}) => { + const baseClasses = + 'font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center'; + + const variantClasses = { + primary: 'bg-primary text-primary-foreground hover:bg-primary/90 focus:ring-ring shadow-md hover:shadow-lg', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 focus:ring-ring', + danger: 'bg-destructive text-destructive-foreground hover:bg-destructive/90 focus:ring-destructive', + ghost: 'hover:bg-accent hover:text-accent-foreground focus:ring-ring', + }; + + const sizeClasses = { + sm: 'px-3 py-2 text-sm', + md: 'px-4 py-2.5 text-base', + lg: 'px-6 py-3 text-lg', + }; + + return ( + + ); +}; diff --git a/apps/appkit-next/src/core/components/common/card.tsx b/apps/appkit-next/src/core/components/common/card.tsx new file mode 100644 index 000000000..43880042e --- /dev/null +++ b/apps/appkit-next/src/core/components/common/card.tsx @@ -0,0 +1,32 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC, ComponentProps } from 'react'; + +import { cn } from '@/core/lib/utils'; + +interface CardProps extends ComponentProps<'div'> { + title?: string; +} + +export const Card: FC = ({ children, className, title, ...props }) => { + return ( +
+ {title && ( +
+

{title}

+
+ )} + +
{children}
+
+ ); +}; diff --git a/apps/appkit-next/src/core/components/common/index.ts b/apps/appkit-next/src/core/components/common/index.ts new file mode 100644 index 000000000..a19bc8d08 --- /dev/null +++ b/apps/appkit-next/src/core/components/common/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { Button } from './button'; +export { Card } from './card'; diff --git a/apps/appkit-next/src/core/components/index.ts b/apps/appkit-next/src/core/components/index.ts new file mode 100644 index 000000000..8e4ac9860 --- /dev/null +++ b/apps/appkit-next/src/core/components/index.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +// Common components +export { Button, Card } from './common'; + +// Layout components +export { Layout, ThemeProvider } from './layout'; diff --git a/apps/appkit-next/src/core/components/layout/index.ts b/apps/appkit-next/src/core/components/layout/index.ts new file mode 100644 index 000000000..9192bf1ff --- /dev/null +++ b/apps/appkit-next/src/core/components/layout/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { Layout } from './layout'; +export { ThemeProvider } from './theme-provider'; diff --git a/apps/appkit-next/src/core/components/layout/layout.tsx b/apps/appkit-next/src/core/components/layout/layout.tsx new file mode 100644 index 000000000..2ed94bba9 --- /dev/null +++ b/apps/appkit-next/src/core/components/layout/layout.tsx @@ -0,0 +1,49 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type React from 'react'; +import { TonConnectButton } from '@ton/appkit-react'; +import { Layers } from 'lucide-react'; + +import { ThemeSwitcher } from './theme-switcher'; + +import { NetworkPicker } from '@/features/network'; + +interface LayoutProps { + children: React.ReactNode; + title?: string; +} + +export const Layout: React.FC = ({ children, title = 'NFT Minter' }) => { + return ( +
+
+
+
+
+ +
+

{title}

+
+ +
+ + + +
+
+
+ +
{children}
+ +
+

Powered by AppKit & TonConnect

+
+
+ ); +}; diff --git a/apps/appkit-next/src/core/components/layout/theme-provider.tsx b/apps/appkit-next/src/core/components/layout/theme-provider.tsx new file mode 100644 index 000000000..854fa5a45 --- /dev/null +++ b/apps/appkit-next/src/core/components/layout/theme-provider.tsx @@ -0,0 +1,94 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use client'; + +import React, { createContext, useEffect, useMemo, useState } from 'react'; + +type Theme = 'dark' | 'light'; +type ThemeOption = 'dark' | 'light' | 'system'; + +type ThemeProviderProps = { + children: React.ReactNode; + defaultTheme?: ThemeOption; + storageKey?: string; +}; + +type ThemeProviderState = { + theme: ThemeOption; + calculatedTheme: Theme; + setTheme: (theme: ThemeOption) => void; +}; + +const initialState: ThemeProviderState = { + theme: 'system', + calculatedTheme: 'light', + setTheme: () => null, +}; + +export const ThemeProviderContext = createContext(initialState); + +export function ThemeProvider({ + children, + defaultTheme = 'system', + storageKey = 'vite-ui-theme', + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState(() => { + if (typeof window !== 'undefined') { + return (window.localStorage.getItem(storageKey) as ThemeOption) || defaultTheme; + } + return defaultTheme; + }); + const calculatedTheme: Theme = useMemo(() => { + if (theme === 'system') { + if (typeof window !== 'undefined') { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + return 'light'; + } + return theme === 'dark' ? 'dark' : 'light'; + }, [theme]); + + useEffect(() => { + const root = window.document.documentElement; + + root.classList.remove('light', 'dark'); + + if (theme === 'system') { + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + + root.classList.add(systemTheme); + root.setAttribute('data-theme', systemTheme); + return; + } + + root.classList.add(theme); + if (theme === 'light') { + root.classList.remove('dark'); + } + root.setAttribute('data-theme', theme); + }, [theme]); + + const value = { + theme, + calculatedTheme, + setTheme: (theme: ThemeOption) => { + if (typeof window !== 'undefined') { + window.localStorage.setItem(storageKey, theme); + } + setTheme(theme); + }, + }; + + return ( + + {children} + + ); +} diff --git a/apps/appkit-next/src/core/components/layout/theme-switcher.tsx b/apps/appkit-next/src/core/components/layout/theme-switcher.tsx new file mode 100644 index 000000000..af95559f0 --- /dev/null +++ b/apps/appkit-next/src/core/components/layout/theme-switcher.tsx @@ -0,0 +1,27 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use client'; + +import { Moon, Sun } from 'lucide-react'; + +import { useTheme } from '@/core/hooks'; + +export function ThemeSwitcher() { + const { theme, setTheme } = useTheme(); + + return ( + + ); +} diff --git a/apps/appkit-next/src/core/configs/app-kit.ts b/apps/appkit-next/src/core/configs/app-kit.ts new file mode 100644 index 000000000..86beda41b --- /dev/null +++ b/apps/appkit-next/src/core/configs/app-kit.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { AppKit, Network, createTonConnectConnector, ApiClientTonApi } from '@ton/appkit'; +import { OmnistonSwapProvider } from '@ton/appkit/swap/omniston'; + +import { ENV_TON_API_KEY_TESTNET, ENV_TON_API_KEY_MAINNET } from '@/core/configs/env'; + +export const appKit = new AppKit({ + networks: { + [Network.mainnet().chainId]: { + apiClient: { + url: 'https://toncenter.com', + key: ENV_TON_API_KEY_MAINNET, + }, + }, + [Network.testnet().chainId]: { + apiClient: { + url: 'https://testnet.toncenter.com', + key: ENV_TON_API_KEY_TESTNET, + }, + }, + [Network.tetra().chainId]: { + apiClient: new ApiClientTonApi({ + network: Network.tetra(), + endpoint: 'https://tetra.tonapi.io', + }), + }, + }, + connectors: [ + createTonConnectConnector({ + tonConnectOptions: { + manifestUrl: 'https://tonconnect-sdk-demo-dapp.vercel.app/tonconnect-manifest.json', + }, + }), + ], + providers: [new OmnistonSwapProvider()], + ssr: true, +}); diff --git a/apps/appkit-next/src/core/configs/env.ts b/apps/appkit-next/src/core/configs/env.ts new file mode 100644 index 000000000..7e58e3ad2 --- /dev/null +++ b/apps/appkit-next/src/core/configs/env.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export const ENV_TON_API_KEY_MAINNET = + process.env.NEXT_PUBLIC_TON_API_KEY ?? '25a9b2326a34b39a5fa4b264fb78fb4709e1bd576fc5e6b176639f5b71e94b0d'; +export const ENV_TON_API_KEY_TESTNET = + process.env.NEXT_PUBLIC_TON_API_TESTNET_KEY ?? 'd852b54d062f631565761042cccea87fa6337c41eb19b075e6c7fb88898a3992'; diff --git a/apps/appkit-next/src/core/configs/query.ts b/apps/appkit-next/src/core/configs/query.ts new file mode 100644 index 000000000..464b48ca0 --- /dev/null +++ b/apps/appkit-next/src/core/configs/query.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { QueryClient } from '@tanstack/react-query'; + +export const queryClient = new QueryClient(); diff --git a/apps/appkit-next/src/core/contexts/context.tsx b/apps/appkit-next/src/core/contexts/context.tsx new file mode 100644 index 000000000..c2cd30aa5 --- /dev/null +++ b/apps/appkit-next/src/core/contexts/context.tsx @@ -0,0 +1,31 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use client'; + +import { AppKitProvider } from '@ton/appkit-react'; +import { QueryClientProvider } from '@tanstack/react-query'; +import type { ReactNode } from 'react'; +import { Toaster } from 'sonner'; + +import { appKit } from '../configs/app-kit'; +import { queryClient } from '../configs/query'; +import { ThemeProvider } from '../components'; + +export default function AppKitContext({ children }: { children: ReactNode }) { + return ( + + + + {children} + + + + + ); +} diff --git a/apps/appkit-next/src/core/hooks/index.ts b/apps/appkit-next/src/core/hooks/index.ts new file mode 100644 index 000000000..29a891e12 --- /dev/null +++ b/apps/appkit-next/src/core/hooks/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { useTheme } from './use-theme'; diff --git a/apps/appkit-next/src/core/hooks/use-theme.ts b/apps/appkit-next/src/core/hooks/use-theme.ts new file mode 100644 index 000000000..47763a3ad --- /dev/null +++ b/apps/appkit-next/src/core/hooks/use-theme.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use client'; + +import { useContext, useEffect } from 'react'; +import { useAppKitTheme } from '@ton/appkit-react'; + +import { ThemeProviderContext } from '@/core/components/layout/theme-provider'; + +export const useTheme = () => { + const context = useContext(ThemeProviderContext); + const [, setTheme] = useAppKitTheme(); + + if (context === undefined) throw new Error('useTheme must be used within a ThemeProvider'); + + useEffect(() => { + setTheme(context.theme); + }, [context.theme]); + + return context; +}; diff --git a/apps/appkit-next/src/core/lib/utils.ts b/apps/appkit-next/src/core/lib/utils.ts new file mode 100644 index 000000000..32eabbb95 --- /dev/null +++ b/apps/appkit-next/src/core/lib/utils.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { clsx } from 'clsx'; +import type { ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export function formatAddress(address: string): string { + if (address.length <= 10) return address; + return `${address.slice(0, 6)}...${address.slice(-4)}`; +} + +export function generateId(): string { + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; +} diff --git a/apps/appkit-next/src/core/pages/minter-page.tsx b/apps/appkit-next/src/core/pages/minter-page.tsx new file mode 100644 index 000000000..0ab7fe561 --- /dev/null +++ b/apps/appkit-next/src/core/pages/minter-page.tsx @@ -0,0 +1,49 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use client'; + +import type React from 'react'; +import { useSelectedWallet } from '@ton/appkit-react'; + +import { TokensCard } from '@/features/balances'; +import { CardGenerator } from '@/features/mint'; +import { NftsCard } from '@/features/nft'; +import { WalletInfo } from '@/features/wallet'; +import { Layout } from '@/core/components'; +import { SwapButton } from '@/features/swap'; +import { SignMessageCard } from '@/features/signing'; + +export const MinterPage: React.FC = () => { + const [wallet] = useSelectedWallet(); + const isConnected = !!wallet; + + return ( + +
+ + + {/* Card Generator with integrated mint button */} + + + {/* Connected wallet assets */} + {isConnected && ( +
+ + + +
+

Swap Demo

+ +
+
+ )} +
+
+ ); +}; diff --git a/apps/appkit-next/src/core/styles/app.css b/apps/appkit-next/src/core/styles/app.css new file mode 100644 index 000000000..d631e3f75 --- /dev/null +++ b/apps/appkit-next/src/core/styles/app.css @@ -0,0 +1,189 @@ +@import "tailwindcss"; + +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +#root { + width: 100%; +} + +/* Rarity glow effects */ +.rarity-common { + --rarity-color: #9ca3af; + --rarity-glow: rgba(156, 163, 175, 0.3); +} + +.rarity-rare { + --rarity-color: #3b82f6; + --rarity-glow: rgba(59, 130, 246, 0.4); +} + +.rarity-epic { + --rarity-color: #8b5cf6; + --rarity-glow: rgba(139, 92, 246, 0.5); +} + +.rarity-legendary { + --rarity-color: #f59e0b; + --rarity-glow: rgba(245, 158, 11, 0.6); +} + +/* Card glow animation */ +.card-glow { + box-shadow: 0 0 20px var(--rarity-glow), 0 0 40px var(--rarity-glow); +} + +.card-glow-legendary { + animation: legendary-glow 2s ease-in-out infinite; +} + +@keyframes legendary-glow { + 0%, 100% { + box-shadow: 0 0 20px rgba(245, 158, 11, 0.6), 0 0 40px rgba(245, 158, 11, 0.4), 0 0 60px rgba(245, 158, 11, 0.2); + } + 50% { + box-shadow: 0 0 30px rgba(245, 158, 11, 0.8), 0 0 60px rgba(245, 158, 11, 0.6), 0 0 90px rgba(245, 158, 11, 0.4); + } +} + +/* Shimmer effect for legendary cards */ +.shimmer-overlay { + background: linear-gradient( + 90deg, + transparent 0%, + rgba(255, 255, 255, 0.2) 50%, + transparent 100% + ); + background-size: 200% 100%; + animation: shimmer 2s linear infinite; +} + +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +@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); + --font-sans: 'Inter Variable', sans-serif; + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); +} + +:root { + --radius: 0.625rem; + --card: oklch(1 0 0); + --card-foreground: oklch(0.147 0.004 49.25); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.147 0.004 49.25); + --primary: oklch(0.6543 0.1605 243.75); + --primary-foreground: oklch(0.98 0.02 201); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.97 0.001 106.424); + --muted-foreground: oklch(0.553 0.013 58.071); + --accent: oklch(0.6543 0.1605 243.75); + --accent-foreground: oklch(0.98 0.02 201); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.923 0.003 48.717); + --input: oklch(0.923 0.003 48.717); + --ring: oklch(0.709 0.01 56.259); + --background: oklch(1 0 0); + --foreground: oklch(0.147 0.004 49.25); + --chart-1: oklch(0.87 0.12 207); + --chart-2: oklch(0.80 0.13 212); + --chart-3: oklch(0.71 0.13 215); + --chart-4: oklch(0.6543 0.1605 243.75); + --chart-5: oklch(0.52 0.09 223); + --sidebar: oklch(0.985 0.001 106.423); + --sidebar-foreground: oklch(0.147 0.004 49.25); + --sidebar-primary: oklch(0.6543 0.1605 243.75); + --sidebar-primary-foreground: oklch(0.98 0.02 201); + --sidebar-accent: oklch(0.6543 0.1605 243.75); + --sidebar-accent-foreground: oklch(0.98 0.02 201); + --sidebar-border: oklch(0.923 0.003 48.717); + --sidebar-ring: oklch(0.709 0.01 56.259); +} + +.dark { + --background: oklch(0.147 0.004 49.25); + --foreground: oklch(0.985 0.001 106.423); + --card: oklch(0.216 0.006 56.043); + --card-foreground: oklch(0.985 0.001 106.423); + --popover: oklch(0.216 0.006 56.043); + --popover-foreground: oklch(0.985 0.001 106.423); + --primary: oklch(0.71 0.13 215); + --primary-foreground: oklch(0.30 0.05 230); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.268 0.007 34.298); + --muted-foreground: oklch(0.709 0.01 56.259); + --accent: oklch(0.71 0.13 215); + --accent-foreground: oklch(0.30 0.05 230); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.553 0.013 58.071); + --chart-1: oklch(0.87 0.12 207); + --chart-2: oklch(0.80 0.13 212); + --chart-3: oklch(0.71 0.13 215); + --chart-4: oklch(0.6543 0.1605 243.75); + --chart-5: oklch(0.52 0.09 223); + --sidebar: oklch(0.216 0.006 56.043); + --sidebar-foreground: oklch(0.985 0.001 106.423); + --sidebar-primary: oklch(0.80 0.13 212); + --sidebar-primary-foreground: oklch(0.30 0.05 230); + --sidebar-accent: oklch(0.71 0.13 215); + --sidebar-accent-foreground: oklch(0.30 0.05 230); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.553 0.013 58.071); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/appkit-next/src/core/styles/index.css b/apps/appkit-next/src/core/styles/index.css new file mode 100644 index 000000000..509ca420e --- /dev/null +++ b/apps/appkit-next/src/core/styles/index.css @@ -0,0 +1,21 @@ +:root { + font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + width: 100%; + min-height: 100vh; +} diff --git a/apps/appkit-next/src/features/balances/components/token-transfer-modal.tsx b/apps/appkit-next/src/features/balances/components/token-transfer-modal.tsx new file mode 100644 index 000000000..a713168b6 --- /dev/null +++ b/apps/appkit-next/src/features/balances/components/token-transfer-modal.tsx @@ -0,0 +1,229 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use client'; + +import React, { useMemo, useState } from 'react'; +import type { Jetton } from '@ton/appkit'; +import { getFormattedJettonInfo, getErrorMessage } from '@ton/appkit'; +import { SendTonButton, SendJettonButton } from '@ton/appkit-react'; +import { Gem, X } from 'lucide-react'; + +import { Button } from '@/core/components'; +import { TransactionStatus } from '@/features/transaction'; + +interface TokenTransferModalProps { + tokenType: 'TON' | 'JETTON'; + jetton?: Jetton; + tonBalance: string; + isOpen: boolean; + onClose: () => void; +} + +export const TokenTransferModal: React.FC = ({ + tokenType, + jetton, + tonBalance, + isOpen, + onClose, +}) => { + const [recipientAddress, setRecipientAddress] = useState(''); + const [amount, setAmount] = useState(''); + const [comment, setComment] = useState(''); + const [transferError, setTransferError] = useState(null); + const [txBoc, setTxBoc] = useState(null); + + const tokenInfo = useMemo(() => { + if (tokenType === 'TON') { + return { + name: 'Toncoin', + symbol: 'TON', + decimals: 9, + balance: tonBalance, + image: './ton.png', + address: null, + }; + } + + if (!jetton) { + throw new Error('Jetton not found'); + } + + const jettonInfo = getFormattedJettonInfo(jetton); + + return { + name: jettonInfo.name, + symbol: jettonInfo.symbol, + decimals: jettonInfo.decimals, + balance: jettonInfo.balance, + image: jettonInfo.image, + address: jettonInfo.address, + }; + }, [tokenType, tonBalance, jetton]); + + const handleClose = () => { + setRecipientAddress(''); + setAmount(''); + setComment(''); + setTransferError(null); + setTxBoc(null); + onClose(); + }; + + if (!isOpen || !tokenInfo.decimals) return null; + + return ( +
+
+
+
+
+
+ {tokenInfo.image ? ( + {tokenInfo.name} + ) : tokenType === 'TON' ? ( + + ) : ( + + {tokenInfo.symbol?.slice(0, 2)} + + )} +
+
+

Transfer {tokenInfo.name}

+

+ Balance: {tokenInfo.balance} {tokenInfo.symbol} +

+
+
+ +
+ + {txBoc ? ( +
+ + +
+ ) : ( + <> +
+
+ + setRecipientAddress(e.target.value)} + placeholder="Enter TON address" + className="w-full px-3 py-2 bg-input border border-border rounded-lg focus:ring-2 focus:ring-ring focus:border-ring text-sm text-foreground placeholder:text-muted-foreground" + /> +
+ +
+ + setAmount(e.target.value)} + placeholder="0.00" + step="any" + min="0" + className="w-full px-3 py-2 bg-input border border-border rounded-lg focus:ring-2 focus:ring-ring focus:border-ring text-sm text-foreground placeholder:text-muted-foreground" + /> +
+ +
+ + setComment(e.target.value)} + placeholder="Add a comment" + className="w-full px-3 py-2 bg-input border border-border rounded-lg focus:ring-2 focus:ring-ring focus:border-ring text-sm text-foreground placeholder:text-muted-foreground" + /> +
+ + {transferError && ( +
+

{transferError}

+
+ )} +
+ +
+ {tokenType === 'TON' && ( + setTransferError(getErrorMessage(error))} + onSuccess={(data) => setTxBoc(data.boc)} + > + {({ isLoading, onSubmit, disabled, text }) => ( + + )} + + )} + + {tokenType === 'JETTON' && jetton?.address && ( + setTransferError(getErrorMessage(error))} + onSuccess={(data) => setTxBoc(data.boc)} + > + {({ isLoading, onSubmit, disabled, text }) => ( + + )} + + )} + + +
+ + )} +
+
+
+ ); +}; diff --git a/apps/appkit-next/src/features/balances/components/tokens-card.tsx b/apps/appkit-next/src/features/balances/components/tokens-card.tsx new file mode 100644 index 000000000..8a62bf899 --- /dev/null +++ b/apps/appkit-next/src/features/balances/components/tokens-card.tsx @@ -0,0 +1,138 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use client'; + +import { useMemo, useState } from 'react'; +import type { FC, ComponentProps } from 'react'; +import type { Jetton } from '@ton/appkit'; +import { getFormattedJettonInfo } from '@ton/appkit'; +import { CurrencyItem, useJettons, useBalance } from '@ton/appkit-react'; +import { AlertCircle } from 'lucide-react'; + +import { TokenTransferModal } from './token-transfer-modal'; + +import { Card, Button } from '@/core/components'; + +interface SelectedToken { + type: 'TON' | 'JETTON'; + jetton?: Jetton; +} + +export const TokensCard: FC> = (props) => { + const [selectedToken, setSelectedToken] = useState(null); + + const { + data: balance, + isLoading: isBalanceLoading, + isError: isBalanceError, + } = useBalance({ query: { refetchInterval: 10000 } }); + + const { + data: jettonsResponse, + isLoading: isJettonsLoading, + isError: isJettonsError, + refetch: onRefresh, + } = useJettons({ query: { refetchInterval: 10000 } }); + + const jettons = useMemo(() => jettonsResponse?.jettons ?? [], [jettonsResponse?.jettons]); + + const isLoading = isBalanceLoading || isJettonsLoading; + const isError = isBalanceError || isJettonsError; + + const totalTokens = jettons.length + 1; // +1 for TON + + if (isError) { + return ( + +
+
+ +
+ +

Failed to load balances

+ + +
+
+ ); + } + + return ( + <> + + {isLoading ? ( +
+
+ Loading balances... +
+ ) : ( +
+ {/* Summary */} +
+

+ {totalTokens} {totalTokens === 1 ? 'Asset' : 'Assets'} +

+ +
+ + {/* Token List */} +
+ setSelectedToken({ type: 'TON' })} + icon="./ton.png" + isVerified + /> + + {/* Jettons */} + {jettons.map((jetton) => { + const info = getFormattedJettonInfo(jetton); + + if (!info || !info.symbol) { + return null; + } + + return ( + setSelectedToken({ type: 'JETTON', jetton })} + /> + ); + })} +
+
+ )} +
+ + {/* Token Transfer Modal */} + {selectedToken && ( + setSelectedToken(null)} + /> + )} + + ); +}; diff --git a/apps/appkit-next/src/features/balances/index.ts b/apps/appkit-next/src/features/balances/index.ts new file mode 100644 index 000000000..49fef305b --- /dev/null +++ b/apps/appkit-next/src/features/balances/index.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +// Components +export { TokensCard } from './components/tokens-card'; +export { TokenTransferModal } from './components/token-transfer-modal'; diff --git a/apps/appkit-next/src/features/mint/components/card-generator.tsx b/apps/appkit-next/src/features/mint/components/card-generator.tsx new file mode 100644 index 000000000..7b67d18a4 --- /dev/null +++ b/apps/appkit-next/src/features/mint/components/card-generator.tsx @@ -0,0 +1,130 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use client'; + +import { useState } from 'react'; +import type React from 'react'; +import { Sparkles, Coins, AlertCircle } from 'lucide-react'; +import { useSelectedWallet, Transaction } from '@ton/appkit-react'; +import { getErrorMessage } from '@ton/appkit'; +import { toast } from 'sonner'; + +import { CardPreview } from './card-preview'; +import { useCardGenerator } from '../hooks/use-card-generator'; +import { useNftMintTransaction } from '../hooks/use-nft-mint-transaction'; +import { mintCard } from '../store/actions/mint-card'; +import { setMintError } from '../store/actions/set-mint-error'; + +import { Button, Card } from '@/core/components'; + +interface CardGeneratorProps { + className?: string; +} + +export const CardGenerator: React.FC = ({ className }) => { + const { currentCard, isGenerating, generate } = useCardGenerator(); + const { createMintTransaction, canMint } = useNftMintTransaction(); + const [wallet] = useSelectedWallet(); + const [mintErrorLocal, setMintErrorLocal] = useState(null); + const isConnected = !!wallet; + + return ( + +
+ {/* Card preview area */} +
+ {currentCard ? ( +
+ +
+ ) : ( +
+
+
+ +

Your card will appear here

+
+
+
+
+ )} +
+ + {/* Rarity odds info */} +
+
+
+
+ 60% +
+
+
+ 25% +
+
+
+ 12% +
+
+
+ 3% +
+
+
+ + {/* Mint error */} + {mintErrorLocal && ( +
+ +

{mintErrorLocal}

+
+ )} + + {/* Action buttons */} +
+ + + {isConnected && canMint && ( + { + mintCard(); + setMintErrorLocal(null); + setMintError(null); + toast.success('NFT minted successfully!'); + }} + onError={(error) => { + const msg = getErrorMessage(error); + setMintErrorLocal(msg); + setMintError(msg); + }} + disabled={!canMint} + > + {({ isLoading, onSubmit, disabled }) => ( + + )} + + )} +
+
+ + ); +}; diff --git a/apps/appkit-next/src/features/mint/components/card-preview.tsx b/apps/appkit-next/src/features/mint/components/card-preview.tsx new file mode 100644 index 000000000..d6f668f49 --- /dev/null +++ b/apps/appkit-next/src/features/mint/components/card-preview.tsx @@ -0,0 +1,116 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use client'; + +import type React from 'react'; +import { Image as ImageIcon } from 'lucide-react'; + +import { RarityBadge } from './rarity-badge'; +import { RarityValues, RARITY_CONFIGS } from '../types/card'; +import type { CardData, Rarity } from '../types/card'; + +import { cn } from '@/core/lib/utils'; + +const borderStyles: Record = { + [RarityValues.Common]: 'border-gray-300', + [RarityValues.Rare]: 'border-blue-400', + [RarityValues.Epic]: 'border-purple-500', + [RarityValues.Legendary]: 'border-amber-400', +}; + +const bgStyles: Record = { + [RarityValues.Common]: 'bg-gradient-to-br from-gray-50 to-gray-100', + [RarityValues.Rare]: 'bg-gradient-to-br from-blue-50 to-blue-100', + [RarityValues.Epic]: 'bg-gradient-to-br from-purple-50 to-purple-100', + [RarityValues.Legendary]: 'bg-gradient-to-br from-amber-50 via-yellow-50 to-orange-50', +}; + +interface CardPreviewProps { + card: CardData; + className?: string; +} + +export const CardPreview: React.FC = ({ card, className }) => { + const config = RARITY_CONFIGS[card.rarity]; + const isLegendary = card.rarity === RarityValues.Legendary; + + return ( +
+ {/* Shimmer overlay for legendary cards */} + {isLegendary &&
} + + {/* Card content */} +
+ {/* Card image */} +
+ {card.imageUrl ? ( + {card.name} + ) : ( +
+ +
+ )} + + {/* Rarity badge overlay */} +
+ +
+
+ + {/* Card info */} +
+

+ {card.name} +

+ + {card.description && ( +

+ {card.description} +

+ )} +
+
+ + {/* Decorative corner elements for epic/legendary */} + {/* {(card.rarity === RarityValues.Epic || card.rarity === RarityValues.Legendary) && ( + <> +
+
+
+
+ + )} */} +
+ ); +}; diff --git a/apps/appkit-next/src/features/mint/components/rarity-badge.tsx b/apps/appkit-next/src/features/mint/components/rarity-badge.tsx new file mode 100644 index 000000000..c7471aa9b --- /dev/null +++ b/apps/appkit-next/src/features/mint/components/rarity-badge.tsx @@ -0,0 +1,49 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use client'; + +import type React from 'react'; + +import { RarityValues, RARITY_CONFIGS } from '../types/card'; +import type { Rarity } from '../types/card'; + +import { cn } from '@/core/lib/utils'; + +interface RarityBadgeProps { + rarity: Rarity; + className?: string; +} + +export const RarityBadge: React.FC = ({ rarity, className }) => { + const config = RARITY_CONFIGS[rarity]; + + const badgeStyles: Record = { + [RarityValues.Common]: 'bg-muted text-muted-foreground border-border', + [RarityValues.Rare]: + 'bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700', + [RarityValues.Epic]: + 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-900/30 dark:text-purple-300 dark:border-purple-700', + [RarityValues.Legendary]: + 'bg-amber-100 text-amber-700 border-amber-300 dark:bg-amber-900/30 dark:text-amber-300 dark:border-amber-700', + }; + + return ( + + {rarity === RarityValues.Legendary && } + {rarity} + + ); +}; diff --git a/apps/appkit-next/src/features/mint/contracts/index.ts b/apps/appkit-next/src/features/mint/contracts/index.ts new file mode 100644 index 000000000..722f2dbc1 --- /dev/null +++ b/apps/appkit-next/src/features/mint/contracts/index.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { makeSnakeCell } from './snake-cell'; +export { encodeOffChainContent, encodeOnChainContent } from './nft-content'; +export type { NftMetadata } from './nft-content'; +export { NftSingleCodeCell, buildSingleNftDataCell, buildSingleNftStateInit } from './nft-single'; +export type { RoyaltyParams, NftSingleData } from './nft-single'; diff --git a/apps/appkit-next/src/features/mint/contracts/nft-content.ts b/apps/appkit-next/src/features/mint/contracts/nft-content.ts new file mode 100644 index 000000000..243397bd2 --- /dev/null +++ b/apps/appkit-next/src/features/mint/contracts/nft-content.ts @@ -0,0 +1,79 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { beginCell, Dictionary } from '@ton/core'; +import type { Cell } from '@ton/core'; +import { sha256_sync } from '@ton/crypto'; + +import { makeSnakeCell } from './snake-cell'; + +const OFF_CHAIN_CONTENT_PREFIX = 0x01; +const ON_CHAIN_CONTENT_PREFIX = 0x00; +const SNAKE_PREFIX = 0x00; + +/** + * Encode off-chain content for NFT metadata + * Format: 0x01 prefix + URL as snake cell + */ +export function encodeOffChainContent(content: string): Cell { + let data = Buffer.from(content); + const offChainPrefix = Buffer.from([OFF_CHAIN_CONTENT_PREFIX]); + data = Buffer.concat([offChainPrefix, data]); + return makeSnakeCell(data); +} + +/** + * NFT metadata for on-chain content + */ +export interface NftMetadata { + name: string; + description?: string; + image?: string; + imageData?: string; // base64 encoded image data +} + +/** + * Encode on-chain content for NFT metadata + * Format: 0x00 prefix + Dictionary + */ +export function encodeOnChainContent(metadata: NftMetadata): Cell { + // Create dictionary with Buffer(32) keys (SHA256 hashes) + const dict = Dictionary.empty(Dictionary.Keys.Buffer(32), { + serialize: (src: Cell, builder) => { + builder.storeRef(src); + }, + parse: (src) => src.loadRef(), + }); + + // Helper to add a field to dictionary + const addField = (key: string, value: string) => { + const keyHash = sha256_sync(key); + // Value is stored as snake cell with 0x00 prefix + const valueData = Buffer.concat([Buffer.from([SNAKE_PREFIX]), Buffer.from(value, 'utf-8')]); + const valueCell = makeSnakeCell(valueData); + dict.set(keyHash, valueCell); + }; + + // Add metadata fields + addField('name', metadata.name); + + if (metadata.description) { + addField('description', metadata.description); + } + + if (metadata.image) { + addField('image', metadata.image); + } + + if (metadata.imageData) { + addField('image_data', metadata.imageData); + } + + // Build content cell: 0x00 prefix + dictionary + return beginCell().storeUint(ON_CHAIN_CONTENT_PREFIX, 8).storeDict(dict).endCell(); +} diff --git a/apps/appkit-next/src/features/mint/contracts/nft-single.ts b/apps/appkit-next/src/features/mint/contracts/nft-single.ts new file mode 100644 index 000000000..fa457f9b3 --- /dev/null +++ b/apps/appkit-next/src/features/mint/contracts/nft-single.ts @@ -0,0 +1,70 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { Cell, beginCell, contractAddress, storeStateInit } from '@ton/core'; +import type { StateInit, Address } from '@ton/core'; + +// NFT Single contract bytecode (compiled TEP-62 standard contract) +const NftSingleCodeBoc = + 'te6cckECFQEAAwoAART/APSkE/S88sgLAQIBYgcCAgEgBAMAI7x+f4ARgYuGRlgOS/uAFoICHAIBWAYFABG0Dp4AQgRr4HAAHbXa/gBNhjoaYfph/0gGEAICzgsIAgEgCgkAGzIUATPFljPFszMye1UgABU7UTQ+kD6QNTUMIAIBIA0MABE+kQwcLry4U2AEuQyIccAkl8D4NDTAwFxsJJfA+D6QPpAMfoAMXHXIfoAMfoAMPACBtMf0z+CEF/MPRRSMLqOhzIQRxA2QBXgghAvyyaiUjC64wKCEGk9OVBSMLrjAoIQHARBKlIwuoBMSEQ4BXI6HMhBHEDZAFeAxMjQ1NYIQGgudURK6n1ETxwXy4ZoB1NQwECPwA+BfBIQP8vAPAfZRNscF8uGR+kAh8AH6QNIAMfoAggr68IAboSGUUxWgod4i1wsBwwAgkgahkTbiIML/8uGSIY4+ghBRGkRjyFAKzxZQC88WcSRKFFRGsHCAEMjLBVAHzxZQBfoCFctqEssfyz8ibrOUWM8XAZEy4gHJAfsAEFeUECo4W+IQAIICjjUm8AGCENUydtsQN0UAbXFwgBDIywVQB88WUAX6AhXLahLLH8s/Im6zlFjPFwGRMuIByQH7AJMwMzTiVQLwAwBUFl8GMwHQEoIQqMsArXCAEMjLBVAFzxYk+gIUy2oTyx/LPwHPFsmAQPsAAIYWXwZsInDIywHJcIIQi3cXNSHIy/8D0BPPFhOAQHCAEMjLBVAHzxZQBfoCFctqEssfyz8ibrOUWM8XAZEy4gHJAfsAAfZRN8cF8uGR+kAh8AH6QNIAMfoAggr68IAboSGUUxWgod4i1wsBwwAgkgahkTbiIMIA8uGSIY4+ghAFE42RyFALzxZQC88WcSRLFFRGwHCAEMjLBVAHzxZQBfoCFctqEssfyz8ibrOUWM8XAZEy4gHJAfsAEGeUECo5W+IUAIICjjUm8AGCENUydtsQN0YAbXFwgBDIywVQB88WUAX6AhXLahLLH8s/Im6zlFjPFwGRMuIByQH7AJMwNDTiVQLwA+GNLv4='; + +export const NftSingleCodeCell = Cell.fromBase64(NftSingleCodeBoc); + +export interface RoyaltyParams { + royaltyFactor: number; // numerator + royaltyBase: number; // denominator (usually 1000) + royaltyAddress: Address; +} + +export interface NftSingleData { + ownerAddress: Address; + editorAddress: Address; + contentCell: Cell; // Pre-encoded content cell (on-chain or off-chain) + royaltyParams: RoyaltyParams; +} + +/** + * Build data cell for NFT Single contract + */ +export function buildSingleNftDataCell(data: NftSingleData): Cell { + const royaltyCell = beginCell() + .storeUint(data.royaltyParams.royaltyFactor, 16) + .storeUint(data.royaltyParams.royaltyBase, 16) + .storeAddress(data.royaltyParams.royaltyAddress) + .endCell(); + + return beginCell() + .storeAddress(data.ownerAddress) + .storeAddress(data.editorAddress) + .storeRef(data.contentCell) + .storeRef(royaltyCell) + .endCell(); +} + +/** + * Build StateInit for NFT Single contract + * Returns stateInit cell and calculated contract address + */ +export function buildSingleNftStateInit(data: NftSingleData) { + const dataCell = buildSingleNftDataCell(data); + + const stateInit: StateInit = { + code: NftSingleCodeCell, + data: dataCell, + }; + + const stateInitCell = beginCell().store(storeStateInit(stateInit)).endCell(); + + const address = contractAddress(0, stateInit); + + return { + stateInit, + stateInitCell, + address, + }; +} diff --git a/apps/appkit-next/src/features/mint/contracts/snake-cell.ts b/apps/appkit-next/src/features/mint/contracts/snake-cell.ts new file mode 100644 index 000000000..f17cbfcbc --- /dev/null +++ b/apps/appkit-next/src/features/mint/contracts/snake-cell.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { beginCell } from '@ton/core'; +import type { Cell } from '@ton/core'; + +/** + * Split buffer into chunks of specified size + */ +function bufferToChunks(buff: Buffer, chunkSize: number): Buffer[] { + const chunks: Buffer[] = []; + while (buff.byteLength > 0) { + chunks.push(buff.subarray(0, chunkSize)); + buff = buff.subarray(chunkSize); + } + return chunks; +} + +/** + * Create a snake cell from buffer data + * Snake cells store data across multiple cells linked by refs + */ +export function makeSnakeCell(data: Buffer): Cell { + const chunks = bufferToChunks(data, 127); + + if (chunks.length === 0) { + return beginCell().endCell(); + } + + if (chunks.length === 1) { + return beginCell().storeBuffer(chunks[0]).endCell(); + } + + let curCell = beginCell(); + + for (let i = chunks.length - 1; i >= 0; i--) { + const chunk = chunks[i]; + + curCell.storeBuffer(chunk); + + if (i - 1 >= 0) { + const nextCell = beginCell(); + nextCell.storeRef(curCell.endCell()); + curCell = nextCell; + } + } + + return curCell.endCell(); +} diff --git a/apps/appkit-next/src/features/mint/hooks/use-card-generator.ts b/apps/appkit-next/src/features/mint/hooks/use-card-generator.ts new file mode 100644 index 000000000..f86877d36 --- /dev/null +++ b/apps/appkit-next/src/features/mint/hooks/use-card-generator.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use client'; + +import { useCallback } from 'react'; + +import { generateCard } from '../store/actions/generate-card'; +import { clearCard } from '../store/actions/clear-card'; +import { useMinterStore } from '../store/minter-store'; + +export function useCardGenerator() { + const currentCard = useMinterStore((state) => state.currentCard); + const isGenerating = useMinterStore((state) => state.isGenerating); + + const generate = useCallback(() => { + generateCard(); + }, []); + + const clear = useCallback(() => { + clearCard(); + }, []); + + return { + currentCard, + isGenerating, + generate, + clear, + }; +} diff --git a/apps/appkit-next/src/features/mint/hooks/use-nft-mint-transaction.ts b/apps/appkit-next/src/features/mint/hooks/use-nft-mint-transaction.ts new file mode 100644 index 000000000..a279d83b8 --- /dev/null +++ b/apps/appkit-next/src/features/mint/hooks/use-nft-mint-transaction.ts @@ -0,0 +1,90 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use client'; + +import { useCallback } from 'react'; +import { toNano, Address, beginCell, storeStateInit } from '@ton/core'; +import { useSelectedWallet } from '@ton/appkit-react'; +import type { Base64String, TransactionRequest } from '@ton/appkit'; + +import { useMinterStore } from '../store/minter-store'; +import { buildSingleNftStateInit, encodeOnChainContent } from '../contracts'; + +type UseNftTransactionType = + | { + canMint: true; + createMintTransaction: () => Promise; + } + | { + canMint: false; + createMintTransaction: () => Promise; + }; + +/** + * Hook to create NFT mint transaction request + */ +export function useNftMintTransaction(): UseNftTransactionType { + const currentCard = useMinterStore((state) => state.currentCard); + const [wallet] = useSelectedWallet(); + + const createMintTransaction = useCallback(async (): Promise => { + if (!currentCard || !wallet) { + throw new Error('Cannot mint NFT: No current card or wallet'); + } + + const walletAddress = Address.parse(wallet.getAddress()); + + // Build on-chain NFT metadata content cell + const contentCell = encodeOnChainContent({ + name: currentCard.name, + description: currentCard.description, + image: currentCard.imageUrl, + }); + + // Build NFT StateInit + const { stateInit, address: nftAddress } = buildSingleNftStateInit({ + ownerAddress: walletAddress, + editorAddress: walletAddress, + contentCell, + royaltyParams: { + royaltyFactor: 0, + royaltyBase: 1000, + royaltyAddress: walletAddress, + }, + }); + + // Create deployment message + const stateInitCell = beginCell().store(storeStateInit(stateInit)).endCell(); + + return { + validUntil: Math.floor(Date.now() / 1000) + 600, // 10 minutes + messages: [ + { + address: nftAddress.toString(), + amount: toNano('0.05').toString(), // 0.05 TON for deployment + stateInit: stateInitCell.toBoc().toString('base64') as Base64String, + }, + ], + }; + }, [currentCard, wallet]); + + const canMint = !!currentCard && !!wallet; + + if (canMint) { + return { + createMintTransaction, + canMint: true, + }; + } else { + return { + createMintTransaction: () => Promise.reject(new Error('Cannot mint NFT: No current card or wallet')), + canMint: false, + }; + } +} diff --git a/apps/appkit-next/src/features/mint/index.ts b/apps/appkit-next/src/features/mint/index.ts new file mode 100644 index 000000000..734b90a1e --- /dev/null +++ b/apps/appkit-next/src/features/mint/index.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +// Components +export { CardGenerator } from './components/card-generator'; +export { CardPreview } from './components/card-preview'; +export { RarityBadge } from './components/rarity-badge'; + +// Hooks +export { useCardGenerator } from './hooks/use-card-generator'; +export { useNftMintTransaction } from './hooks/use-nft-mint-transaction'; + +// Store +export { useMinterStore } from './store/minter-store'; + +// Types +export type { CardData, Rarity, RarityConfig } from './types/card'; +export { RarityValues, RARITY_CONFIGS } from './types/card'; diff --git a/apps/appkit-next/src/features/mint/lib/card-data.ts b/apps/appkit-next/src/features/mint/lib/card-data.ts new file mode 100644 index 000000000..b733ec014 --- /dev/null +++ b/apps/appkit-next/src/features/mint/lib/card-data.ts @@ -0,0 +1,122 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { RarityValues, RARITY_CONFIGS } from '../types/card'; +import type { Rarity } from '../types/card'; +import { textToSvgPath, getTextWidth } from './svg-glyphs'; + +// Card names organized by rarity +export const CARD_NAMES: Record = { + [RarityValues.Common]: [ + 'Forest Sprite', + 'Stone Golem', + 'River Nymph', + 'Wind Wisp', + 'Earth Guardian', + 'Flame Imp', + 'Shadow Cat', + 'Crystal Beetle', + 'Moss Troll', + 'Dust Elemental', + ], + [RarityValues.Rare]: [ + 'Storm Drake', + 'Frost Mage', + 'Thunder Wolf', + 'Void Walker', + 'Ember Phoenix', + 'Ocean Serpent', + 'Mountain Giant', + 'Star Gazer', + ], + [RarityValues.Epic]: [ + 'Ancient Dragon', + 'Celestial Knight', + 'Shadow Reaper', + 'Arcane Wizard', + 'Divine Guardian', + 'Chaos Lord', + ], + [RarityValues.Legendary]: ['Eternal Phoenix', 'World Serpent', 'Cosmic Titan', 'Primordial Dragon'], +}; + +// Card descriptions by rarity +export const CARD_DESCRIPTIONS: Record = { + [RarityValues.Common]: [ + 'A humble creature of the wild.', + 'Born from the elements themselves.', + 'A faithful companion on any journey.', + ], + [RarityValues.Rare]: [ + 'A powerful being with hidden potential.', + 'Sought after by collectors across the realm.', + 'Wielding magic beyond ordinary means.', + ], + [RarityValues.Epic]: [ + 'A legendary creature of immense power.', + 'Few have witnessed such magnificence.', + 'Ancient magic flows through its veins.', + ], + [RarityValues.Legendary]: [ + 'A mythical being of unparalleled power.', + 'Said to exist only in legends.', + 'The rarest of all creatures in existence.', + ], +}; + +/** + * Get a random rarity based on configured weights + */ +export function getRandomRarity(): Rarity { + const totalWeight = Object.values(RARITY_CONFIGS).reduce((sum, config) => sum + config.weight, 0); + let random = Math.random() * totalWeight; + + for (const rarity of Object.values(RarityValues)) { + const config = RARITY_CONFIGS[rarity]; + if (random < config.weight) { + return rarity; + } + random -= config.weight; + } + + return RarityValues.Common; +} + +/** + * Get a random name for a given rarity + */ +export function getRandomName(rarity: Rarity): string { + const names = CARD_NAMES[rarity]; + return names[Math.floor(Math.random() * names.length)]; +} + +/** + * Get a random description for a given rarity + */ +export function getRandomDescription(rarity: Rarity): string { + const descriptions = CARD_DESCRIPTIONS[rarity]; + return descriptions[Math.floor(Math.random() * descriptions.length)]; +} + +/** + * Generate a placeholder image URL based on rarity (inline SVG with path-based text) + */ +export async function getCardImageUrl(rarity: Rarity, name: string): Promise { + const config = RARITY_CONFIGS[rarity]; + const fontSize = 18; + const textWidth = await getTextWidth(name, fontSize); + const textX = (300 - textWidth) / 2; + const textY = 200 + fontSize / 3; // Approximate vertical centering + const textPath = await textToSvgPath(name, fontSize, textX, textY); + + const svg = ` + + + `; + return `data:image/svg+xml,${encodeURIComponent(svg)}`; +} diff --git a/apps/appkit-next/src/features/mint/lib/svg-glyphs.ts b/apps/appkit-next/src/features/mint/lib/svg-glyphs.ts new file mode 100644 index 000000000..56373e116 --- /dev/null +++ b/apps/appkit-next/src/features/mint/lib/svg-glyphs.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +// @ts-expect-error - opentype.js doesn't have types +import opentype from 'opentype.js'; + +const FONT_URL = 'https://cdn.jsdelivr.net/gh/rsms/inter@v3.19/docs/font-files/Inter-Regular.otf'; + +let fontPromise: Promise | null = null; + +/** + * Load the Inter font (cached) + */ +function loadFont(): Promise { + if (!fontPromise) { + fontPromise = opentype.load(FONT_URL); + } + return fontPromise!; +} + +/** + * Convert text to SVG path data + */ +export async function textToSvgPath(text: string, fontSize: number, x: number, y: number): Promise { + const font = await loadFont(); + const path = font.getPath(text, x, y, fontSize); + return path.toPathData(2); +} + +/** + * Get the width of text when rendered + */ +export async function getTextWidth(text: string, fontSize: number): Promise { + const font = await loadFont(); + return font.getAdvanceWidth(text, fontSize); +} diff --git a/apps/appkit-next/src/features/mint/store/actions/clear-card.ts b/apps/appkit-next/src/features/mint/store/actions/clear-card.ts new file mode 100644 index 000000000..030a1fef3 --- /dev/null +++ b/apps/appkit-next/src/features/mint/store/actions/clear-card.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useMinterStore } from '../minter-store'; + +export const clearCard = (): void => { + useMinterStore.setState({ currentCard: null, mintError: null }); +}; diff --git a/apps/appkit-next/src/features/mint/store/actions/generate-card.ts b/apps/appkit-next/src/features/mint/store/actions/generate-card.ts new file mode 100644 index 000000000..1be58bafb --- /dev/null +++ b/apps/appkit-next/src/features/mint/store/actions/generate-card.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { getRandomRarity, getRandomName, getRandomDescription, getCardImageUrl } from '../../lib/card-data'; +import type { CardData } from '../../types/card'; +import { useMinterStore } from '../minter-store'; + +import { generateId } from '@/core/lib/utils'; + +export const generateCard = async (): Promise => { + useMinterStore.setState({ isGenerating: true, mintError: null }); + + const rarity = getRandomRarity(); + const name = getRandomName(rarity); + const description = getRandomDescription(rarity); + const imageUrl = await getCardImageUrl(rarity, name); + + const newCard: CardData = { + id: generateId(), + name, + rarity, + description, + imageUrl, + createdAt: Date.now(), + }; + + useMinterStore.setState({ currentCard: newCard, isGenerating: false }); +}; diff --git a/apps/appkit-next/src/features/mint/store/actions/mint-card.ts b/apps/appkit-next/src/features/mint/store/actions/mint-card.ts new file mode 100644 index 000000000..10dfe9b74 --- /dev/null +++ b/apps/appkit-next/src/features/mint/store/actions/mint-card.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useMinterStore } from '../minter-store'; + +export const mintCard = async (): Promise => { + const { currentCard } = useMinterStore.getState(); + if (!currentCard) return; + + useMinterStore.setState({ isMinting: true, mintError: null }); + + try { + // The actual minting will be handled by the wallet hook + // This just updates the local state after successful mint + useMinterStore.setState((state) => ({ + mintedCards: [...state.mintedCards, currentCard], + currentCard: null, + isMinting: false, + })); + } catch (error) { + useMinterStore.setState({ + mintError: error instanceof Error ? error.message : 'Failed to mint card', + isMinting: false, + }); + } +}; diff --git a/apps/appkit-next/src/features/mint/store/actions/set-mint-error.ts b/apps/appkit-next/src/features/mint/store/actions/set-mint-error.ts new file mode 100644 index 000000000..80d0343d3 --- /dev/null +++ b/apps/appkit-next/src/features/mint/store/actions/set-mint-error.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useMinterStore } from '../minter-store'; + +export const setMintError = (error: string | null): void => { + useMinterStore.setState({ mintError: error }); +}; diff --git a/apps/appkit-next/src/features/mint/store/actions/set-minting.ts b/apps/appkit-next/src/features/mint/store/actions/set-minting.ts new file mode 100644 index 000000000..38098c90b --- /dev/null +++ b/apps/appkit-next/src/features/mint/store/actions/set-minting.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useMinterStore } from '../minter-store'; + +export const setMinting = (isMinting: boolean): void => { + useMinterStore.setState({ isMinting }); +}; diff --git a/apps/appkit-next/src/features/mint/store/index.ts b/apps/appkit-next/src/features/mint/store/index.ts new file mode 100644 index 000000000..50559a19e --- /dev/null +++ b/apps/appkit-next/src/features/mint/store/index.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { useMinterStore } from './minter-store'; + +// Actions +export { generateCard } from './actions/generate-card'; +export { mintCard } from './actions/mint-card'; +export { clearCard } from './actions/clear-card'; +export { setMinting } from './actions/set-minting'; +export { setMintError } from './actions/set-mint-error'; diff --git a/apps/appkit-next/src/features/mint/store/minter-store.ts b/apps/appkit-next/src/features/mint/store/minter-store.ts new file mode 100644 index 000000000..bfb749b01 --- /dev/null +++ b/apps/appkit-next/src/features/mint/store/minter-store.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { create } from 'zustand'; + +import type { CardData } from '../types/card'; + +interface MinterState { + currentCard: CardData | null; + mintedCards: CardData[]; + isGenerating: boolean; + isMinting: boolean; + mintError: string | null; +} + +export const useMinterStore = create(() => ({ + currentCard: null, + mintedCards: [], + isGenerating: false, + isMinting: false, + mintError: null, +})); diff --git a/apps/appkit-next/src/features/mint/types/card.ts b/apps/appkit-next/src/features/mint/types/card.ts new file mode 100644 index 000000000..a5d6bf4b7 --- /dev/null +++ b/apps/appkit-next/src/features/mint/types/card.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export const RarityValues = { + Common: 'common', + Rare: 'rare', + Epic: 'epic', + Legendary: 'legendary', +} as const; + +export type Rarity = (typeof RarityValues)[keyof typeof RarityValues]; + +export interface CardData { + id: string; + name: string; + rarity: Rarity; + imageUrl?: string; + description?: string; + createdAt: number; +} + +export interface RarityConfig { + rarity: Rarity; + weight: number; + color: string; + bgGradient: string; + borderColor: string; + glowClass: string; +} + +export const RARITY_CONFIGS: Record = { + [RarityValues.Common]: { + rarity: RarityValues.Common, + weight: 60, + color: '#9ca3af', + bgGradient: 'from-gray-100 to-gray-200', + borderColor: 'border-gray-400', + glowClass: 'rarity-common', + }, + [RarityValues.Rare]: { + rarity: RarityValues.Rare, + weight: 25, + color: '#3b82f6', + bgGradient: 'from-blue-100 to-blue-200', + borderColor: 'border-blue-500', + glowClass: 'rarity-rare', + }, + [RarityValues.Epic]: { + rarity: RarityValues.Epic, + weight: 12, + color: '#8b5cf6', + bgGradient: 'from-purple-100 to-purple-200', + borderColor: 'border-purple-500', + glowClass: 'rarity-epic', + }, + [RarityValues.Legendary]: { + rarity: RarityValues.Legendary, + weight: 3, + color: '#f59e0b', + bgGradient: 'from-amber-100 to-yellow-200', + borderColor: 'border-amber-500', + glowClass: 'rarity-legendary', + }, +}; diff --git a/apps/appkit-next/src/features/network/components/network-picker.tsx b/apps/appkit-next/src/features/network/components/network-picker.tsx new file mode 100644 index 000000000..325f28b36 --- /dev/null +++ b/apps/appkit-next/src/features/network/components/network-picker.tsx @@ -0,0 +1,69 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use client'; + +import type { FC, ComponentProps, ChangeEvent } from 'react'; +import { useDefaultNetwork, useNetworks, useSelectedWallet, Network } from '@ton/appkit-react'; +import { ChevronDown } from 'lucide-react'; + +import { cn } from '@/core/lib/utils'; + +const NETWORK_LABELS: Record = { + [Network.mainnet().chainId]: 'Mainnet', + [Network.testnet().chainId]: 'Testnet', + [Network.tetra().chainId]: 'Tetra', +}; + +const getNetworkLabel = (chainId: string): string => { + return NETWORK_LABELS[chainId] ?? `Chain ${chainId}`; +}; + +export const NetworkPicker: FC> = ({ className, ...props }) => { + const [defaultNetwork, setDefaultNetwork] = useDefaultNetwork(); + const networks = useNetworks(); + const [wallet] = useSelectedWallet(); + + const handleChange = (e: ChangeEvent) => { + const value = e.target.value; + + if (value === '') { + setDefaultNetwork(undefined); + } else { + setDefaultNetwork(Network.custom(value)); + } + }; + + if (wallet) { + return null; + } + + return ( +
+ +
+ +
+
+ ); +}; diff --git a/apps/appkit-next/src/features/network/index.ts b/apps/appkit-next/src/features/network/index.ts new file mode 100644 index 000000000..d3727ee53 --- /dev/null +++ b/apps/appkit-next/src/features/network/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { NetworkPicker } from './components/network-picker'; diff --git a/apps/appkit-next/src/features/nft/components/nft-transfer-modal.tsx b/apps/appkit-next/src/features/nft/components/nft-transfer-modal.tsx new file mode 100644 index 000000000..95091acfa --- /dev/null +++ b/apps/appkit-next/src/features/nft/components/nft-transfer-modal.tsx @@ -0,0 +1,140 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use client'; + +import React, { useCallback, useMemo, useState } from 'react'; +import type { NFT } from '@ton/appkit'; +import { getFormattedNftInfo, createTransferNftTransaction, getErrorMessage } from '@ton/appkit'; +import { Transaction, useAppKit } from '@ton/appkit-react'; +import { toast } from 'sonner'; +import { X, Image as ImageIcon } from 'lucide-react'; + +import { Button } from '@/core/components'; + +interface NftTransferModalProps { + nft: NFT; + isOpen: boolean; + onClose: () => void; +} + +export const NftTransferModal: React.FC = ({ nft, isOpen, onClose }) => { + const [recipientAddress, setRecipientAddress] = useState(''); + const [comment, setComment] = useState(''); + const [transferError, setTransferError] = useState(null); + + const appKit = useAppKit(); + + const nftInfo = useMemo(() => getFormattedNftInfo(nft), [nft]); + + const createTransferTransaction = useCallback(async () => { + return createTransferNftTransaction(appKit, { + nftAddress: nft.address, + recipientAddress, + comment, + }); + }, [appKit, nft.address, recipientAddress, comment]); + + const handleClose = () => { + setRecipientAddress(''); + setComment(''); + setTransferError(null); + onClose(); + }; + + if (!isOpen) return null; + + return ( +
+
+
+
+

Transfer NFT

+ +
+ + {/* NFT Preview */} +
+
+ {nftInfo.image ? ( + {nftInfo.name} + ) : ( + + )} +
+

{nftInfo.name}

+

{nftInfo.collectionName}

+ {nftInfo.description && ( +

{nftInfo.description}

+ )} +
+ +
+
+ + setRecipientAddress(e.target.value)} + placeholder="Enter TON address" + className="w-full px-3 py-2 bg-input border border-border rounded-lg focus:ring-2 focus:ring-ring focus:border-ring text-sm text-foreground placeholder:text-muted-foreground" + /> +
+ +
+ + setComment(e.target.value)} + placeholder="Add a comment" + className="w-full px-3 py-2 bg-input border border-border rounded-lg focus:ring-2 focus:ring-ring focus:border-ring text-sm text-foreground placeholder:text-muted-foreground" + /> +
+ + {transferError && ( +
+

{transferError}

+
+ )} +
+ +
+ { + handleClose(); + toast.success('NFT transferred successfully'); + }} + onError={(error) => { + setTransferError(getErrorMessage(error)); + }} + disabled={!recipientAddress} + > + {({ isLoading, onSubmit, disabled, text }) => ( + + )} + + + +
+
+
+
+ ); +}; diff --git a/apps/appkit-next/src/features/nft/components/nfts-card.tsx b/apps/appkit-next/src/features/nft/components/nfts-card.tsx new file mode 100644 index 000000000..646662573 --- /dev/null +++ b/apps/appkit-next/src/features/nft/components/nfts-card.tsx @@ -0,0 +1,106 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use client'; + +import { useMemo, useState } from 'react'; +import type { FC, ComponentProps } from 'react'; +import type { NFT } from '@ton/appkit'; +import { NftItem, useNfts } from '@ton/appkit-react'; +import { AlertCircle, Image as ImageIcon } from 'lucide-react'; + +import { NftTransferModal } from './nft-transfer-modal'; + +import { Card, Button } from '@/core/components'; + +export const NftsCard: FC> = (props) => { + const [selectedNft, setSelectedNft] = useState(null); + + const { + data: nftsResponse, + isLoading: isLoading, + isError: isError, + refetch: onRefresh, + } = useNfts({ query: { refetchInterval: 10000 } }); + + const nfts = useMemo(() => nftsResponse?.nfts ?? [], [nftsResponse?.nfts]); + + if (isError) { + return ( + +
+
+ +
+ +

Failed to load NFTs

+ + +
+
+ ); + } + + return ( + <> + + {isLoading ? ( +
+
+ Loading NFTs... +
+ ) : nfts.length === 0 ? ( +
+
+ +
+

No NFTs yet

+

Your NFT collection will appear here

+
+ ) : ( +
+ {/* Summary */} +
+

+ {nfts.length} {nfts.length === 1 ? 'NFT' : 'NFTs'} +

+ +
+ + {/* NFT Grid */} +
+ {nfts.slice(0, 8).map((nft) => ( + setSelectedNft(nft)} + /> + ))} +
+ + {nfts.length > 8 && ( +
+

Showing 8 of {nfts.length} NFTs

+
+ )} +
+ )} +
+ + {/* NFT Transfer Modal */} + {selectedNft && ( + setSelectedNft(null)} /> + )} + + ); +}; diff --git a/apps/appkit-next/src/features/nft/index.ts b/apps/appkit-next/src/features/nft/index.ts new file mode 100644 index 000000000..005245b13 --- /dev/null +++ b/apps/appkit-next/src/features/nft/index.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +// Components +export { NftsCard } from './components/nfts-card'; +export { NftTransferModal } from './components/nft-transfer-modal'; diff --git a/apps/appkit-next/src/features/signing/components/sign-message-card.tsx b/apps/appkit-next/src/features/signing/components/sign-message-card.tsx new file mode 100644 index 000000000..567caa610 --- /dev/null +++ b/apps/appkit-next/src/features/signing/components/sign-message-card.tsx @@ -0,0 +1,97 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use client'; + +import { useState } from 'react'; +import type { FC, ComponentProps } from 'react'; +import { useSignText, useSelectedWallet } from '@ton/appkit-react'; +import { toast } from 'sonner'; + +import { Card, Button } from '@/core/components'; + +export const SignMessageCard: FC> = (props) => { + const [message, setMessage] = useState(''); + const [signature, setSignature] = useState(null); + + const [wallet] = useSelectedWallet(); + const { mutate: signText, isPending } = useSignText({ + mutation: { + onSuccess: (result) => { + setSignature(result.signature); + toast.success('Message signed successfully!'); + }, + onError: (error) => { + toast.error(`Signing failed: ${error.message}`); + }, + }, + }); + + const handleSign = () => { + if (!wallet || !message.trim()) { + toast.error('Please enter a message to sign'); + return; + } + + signText({ text: message }); + }; + + const handleCopySignature = () => { + if (signature) { + navigator.clipboard.writeText(signature); + toast.success('Signature copied to clipboard!'); + } + }; + + return ( + +
+ {/* Message Input */} +
+ +