├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── src ├── models │ └── types.ts ├── lib │ └── utils.tsx ├── views │ ├── index.tsx │ ├── home │ │ └── index.tsx │ ├── upload │ │ └── index.tsx │ ├── claim-fees │ │ └── index.tsx │ ├── close │ │ └── index.tsx │ ├── create │ │ └── index.tsx │ └── burn │ │ └── index.tsx ├── config.tsx ├── pages │ ├── api │ │ └── hello.ts │ ├── burn.tsx │ ├── upload.tsx │ ├── create.tsx │ ├── claim-fees.tsx │ ├── close.tsx │ ├── index.tsx │ ├── _app.tsx │ └── _document.tsx ├── utils │ ├── CUPerInstruction.tsx │ ├── getTotalLamports.tsx │ ├── notifications.tsx │ ├── getConnection.tsx │ ├── explorer.ts │ ├── confirmTransaction.tsx │ ├── index.tsx │ ├── getNonEmptyAccounts.tsx │ ├── getEmptyAccounts.tsx │ ├── filterByScamsAndLegit.tsx │ ├── getCloseTransactions.tsx │ ├── getWithdrawTransactions.tsx │ ├── getAssetsInfos.tsx │ └── getBurnAndCloseTransactions.tsx ├── stores │ ├── useNotificationStore.tsx │ └── useUserSOLBalanceStore.tsx ├── styles │ └── globals.css ├── hooks │ └── useQueryContext.tsx ├── components │ ├── NetworkSwitcher.tsx │ ├── Loader.tsx │ ├── Footer.tsx │ ├── ContentContainer.tsx │ ├── Text │ │ └── index.tsx │ ├── nav-element │ │ └── index.tsx │ ├── ui │ │ └── card.tsx │ ├── SignMessage.tsx │ ├── Dropdown │ │ ├── NFTDropdown.tsx │ │ └── TokenDropdown.tsx │ ├── SendVersionedTransaction.tsx │ ├── SendTransaction.tsx │ ├── AppBar.tsx │ └── Notification.tsx └── contexts │ ├── NetworkConfigurationProvider.tsx │ ├── AutoConnectProvider.tsx │ └── ContextProvider.tsx ├── public ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── browserconfig.xml ├── site.webmanifest └── vercel.svg ├── postcss.config.js ├── next-env.d.ts ├── Dockerfile ├── next.config.js ├── .github ├── dependabot.yml └── workflows │ └── frontend.yml ├── .gitignore ├── tsconfig.json ├── package.json ├── tailwind.config.js └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { "presets": ["next/babel"] } 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | __generated__ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/models/types.ts: -------------------------------------------------------------------------------- 1 | export type EndpointTypes = 'mainnet' | 'devnet' | 'localnet' 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Immutal0/Solana-SPL-pNFT-Tools/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Immutal0/Solana-SPL-pNFT-Tools/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Immutal0/Solana-SPL-pNFT-Tools/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Immutal0/Solana-SPL-pNFT-Tools/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Immutal0/Solana-SPL-pNFT-Tools/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Immutal0/Solana-SPL-pNFT-Tools/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/lib/utils.tsx: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine 2 | 3 | WORKDIR /opt/app 4 | 5 | ENV NODE_ENV production 6 | 7 | COPY package*.json ./ 8 | 9 | RUN npm ci 10 | 11 | COPY . /opt/app 12 | 13 | RUN npm install --dev && npm run build 14 | 15 | CMD [ "npm", "start" ] -------------------------------------------------------------------------------- /src/views/index.tsx: -------------------------------------------------------------------------------- 1 | export { HomeView } from "./home"; 2 | export { CreateView } from "./create"; 3 | export { CloseView } from "./close"; 4 | export { BurnView } from "./burn"; 5 | export { UploadView } from "./upload"; 6 | export { ClaimFeesView } from "./claim-fees"; 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | webpack5: true, 5 | webpack: (config) => { 6 | config.resolve.fallback = { fs: false }; 7 | 8 | return config; 9 | }, 10 | } 11 | 12 | module.exports = nextConfig 13 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/config.tsx: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js" 2 | 3 | export const HELIUS_API_KEY = "cc778adb-f9ab-45da-ba44-b4096f663c16" 4 | 5 | export const RPC_URL = `https://mainnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}` 6 | 7 | export const AUTHORITY = new PublicKey("authP96E1taxSvMmrVkNGRvN1NF6ohW95g9P7wZ3iY2"); -------------------------------------------------------------------------------- /src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | 13 | - package-ecosystem: "cargo" 14 | directory: "/program" 15 | schedule: 16 | interval: "daily" 17 | -------------------------------------------------------------------------------- /src/utils/CUPerInstruction.tsx: -------------------------------------------------------------------------------- 1 | export const BURN_CU = 4800; 2 | export const CLOSE_ACCOUNT_CU = 3000; 3 | export const HARVEST_TOKENS_CU = 2000; 4 | export const WITHDRAW_TRANSFER_FEES_FOR_ONE_ACCOUNT_CU = 2000; 5 | export const WITHDRAW_TRANSFER_FEES_FOR_ADDITIONAL_ACCOUNT_CU = 400; 6 | export const ADD_COMPUTE_UNIT_PRICE_CU = 150; 7 | export const ADD_COMPUTE_UNIT_LIMIT_CU = 150; 8 | export const SOL_TRANSFER_CU = 150; -------------------------------------------------------------------------------- /src/utils/getTotalLamports.tsx: -------------------------------------------------------------------------------- 1 | import { LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js"; 2 | 3 | export const getTotalLamports = ( 4 | assets: { 5 | account: PublicKey; 6 | program: PublicKey; 7 | lamports: number; 8 | mint: string 9 | }[] 10 | 11 | ) => { 12 | 13 | let maxRedeem = 0; 14 | assets.map((asset) => maxRedeem += asset.lamports); 15 | maxRedeem = maxRedeem / LAMPORTS_PER_SOL; 16 | return maxRedeem 17 | } -------------------------------------------------------------------------------- /src/pages/burn.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import { BurnView } from "../views"; 4 | 5 | const Burn: NextPage = (props) => { 6 | return ( 7 |
8 | 9 | Solana-Tools 10 | 14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default Burn; 21 | -------------------------------------------------------------------------------- /src/pages/upload.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import { UploadView } from "../views"; 4 | 5 | const Upload: NextPage = (props) => { 6 | return ( 7 |
8 | 9 | Solana-Tools 10 | 14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default Upload; 21 | -------------------------------------------------------------------------------- /src/pages/create.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import { CreateView } from "../views"; 4 | 5 | const Create: NextPage = (props) => { 6 | return ( 7 |
8 | 9 | Solana-Tools 10 | 14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default Create; 21 | -------------------------------------------------------------------------------- /src/pages/claim-fees.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import { ClaimFeesView } from "../views"; 4 | 5 | const ClaimFees: NextPage = (props) => { 6 | return ( 7 |
8 | 9 | Solana-Tools 10 | 14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default ClaimFees; 21 | -------------------------------------------------------------------------------- /src/pages/close.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import { CloseView } from "../views"; 4 | 5 | const Close: NextPage = (props) => { 6 | return ( 7 |
8 | 9 | Solana-Tools 10 | 14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default Close; 21 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import { HomeView } from "../views"; 4 | 5 | const Home: NextPage = (props) => { 6 | return ( 7 |
8 | 9 | Solana-Tools 10 | 14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default Home; 21 | -------------------------------------------------------------------------------- /src/stores/useNotificationStore.tsx: -------------------------------------------------------------------------------- 1 | import create, { State } from "zustand"; 2 | import produce from "immer"; 3 | 4 | interface NotificationStore extends State { 5 | notifications: Array<{ 6 | type: string 7 | message: string 8 | description?: string 9 | txid?: string 10 | }> 11 | set: (x: any) => void 12 | } 13 | 14 | const useNotificationStore = create((set, _get) => ({ 15 | notifications: [], 16 | set: (fn) => set(produce(fn)), 17 | })) 18 | 19 | export default useNotificationStore 20 | -------------------------------------------------------------------------------- /src/utils/notifications.tsx: -------------------------------------------------------------------------------- 1 | import useNotificationStore from "../stores/useNotificationStore"; 2 | 3 | export function notify(newNotification: { 4 | type?: string 5 | message: string 6 | description?: string 7 | txid?: string 8 | }) { 9 | const { 10 | notifications, 11 | set: setNotificationStore, 12 | } = useNotificationStore.getState() 13 | 14 | setNotificationStore((state: { notifications: any[] }) => { 15 | state.notifications = [ 16 | ...notifications, 17 | { type: 'success', ...newNotification }, 18 | ] 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/getConnection.tsx: -------------------------------------------------------------------------------- 1 | import { Connection } from "@solana/web3.js"; 2 | import { RPC_URL } from "config"; 3 | 4 | export function getConnection(networkSelected: string) { 5 | let connection: Connection; 6 | 7 | if (networkSelected == "devnet") { 8 | connection = new Connection("https://api.devnet.solana.com", { 9 | commitment: "confirmed", 10 | }); 11 | } else { 12 | connection = new Connection( 13 | RPC_URL, 14 | { commitment: "confirmed" } 15 | ); 16 | } 17 | 18 | return connection; 19 | } -------------------------------------------------------------------------------- /.github/workflows/frontend.yml: -------------------------------------------------------------------------------- 1 | name: Frontend 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/cache@v2 15 | with: 16 | path: "**/node_modules" 17 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 18 | - uses: actions/setup-node@v2 19 | with: 20 | node-version: "16" 21 | - run: yarn install 22 | - name: Build 23 | run: yarn build 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | 39 | # logs 40 | *.log -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Solana-Tools", 3 | "short_name": "", 4 | "description": "Bunch of free and open source tools to help you on Solana.", 5 | "icons": [ 6 | { 7 | "src": "/android-chrome-192x192.png", 8 | "sizes": "192x192", 9 | "type": "image/png" 10 | }, 11 | { 12 | "src": "/android-chrome-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png" 15 | } 16 | ], 17 | "theme_color": "#ffffff", 18 | "background_color": "#ffffff", 19 | "display": "standalone" 20 | } -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | padding: 0; 8 | margin: 0; 9 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 10 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 11 | background-color: #4f4b48; 12 | } 13 | 14 | a { 15 | color: inherit; 16 | text-decoration: none; 17 | } 18 | 19 | * { 20 | box-sizing: border-box; 21 | } 22 | 23 | /* example: override wallet button style */ 24 | .wallet-adapter-button:not([disabled]):hover { 25 | background-color: #707070; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/hooks/useQueryContext.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router' 2 | import { EndpointTypes } from '../models/types' 3 | 4 | export default function useQueryContext() { 5 | const router = useRouter() 6 | const { cluster } = router.query 7 | 8 | const endpoint = cluster ? (cluster as EndpointTypes) : 'mainnet' 9 | const hasClusterOption = endpoint !== 'mainnet' 10 | const fmtUrlWithCluster = (url) => { 11 | if (hasClusterOption) { 12 | const mark = url.includes('?') ? '&' : '?' 13 | return decodeURIComponent(`${url}${mark}cluster=${endpoint}`) 14 | } 15 | return url 16 | } 17 | 18 | return { 19 | fmtUrlWithCluster, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/explorer.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, Transaction } from '@solana/web3.js' 2 | import base58 from 'bs58' 3 | 4 | export function getExplorerUrl( 5 | endpoint: string, 6 | viewTypeOrItemAddress: 'inspector' | PublicKey | string, 7 | itemType = 'address' // | 'tx' | 'block' 8 | ) { 9 | const getClusterUrlParam = () => { 10 | let cluster = '' 11 | if (endpoint === 'localnet') { 12 | cluster = `custom&customUrl=${encodeURIComponent( 13 | 'http://127.0.0.1:8899' 14 | )}` 15 | } else if (endpoint === 'https://api.devnet.solana.com') { 16 | cluster = 'devnet' 17 | } 18 | 19 | return cluster ? `?cluster=${cluster}` : '' 20 | } 21 | 22 | return `https://explorer.solana.com/${itemType}/${viewTypeOrItemAddress}${getClusterUrlParam()}` 23 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src", 4 | "target": "es6", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": false, 9 | "strictNullChecks": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true 19 | }, 20 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 21 | "exclude": ["node_modules", ".next",], 22 | "ts-node": { 23 | "require": ["tsconfig-paths/register"], 24 | "compilerOptions": { 25 | "module": "commonjs" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/confirmTransaction.tsx: -------------------------------------------------------------------------------- 1 | import { Connection } from "@solana/web3.js"; 2 | import { notify } from "./notifications"; 3 | 4 | export async function confirmTransaction(connection: Connection, signature: string) { 5 | 6 | let confirmed = false; 7 | let timeout = 0; 8 | while (!confirmed && timeout < 10000) { 9 | await new Promise(r => setTimeout(r, 500)); 10 | let status = await connection.getSignatureStatuses([signature]); 11 | console.log(status) 12 | if (status.value[0]?.confirmationStatus == "confirmed") { 13 | notify({ type: 'success', message: `Success!`, txid: signature }); 14 | confirmed = true; 15 | } 16 | else { 17 | timeout += 500; 18 | } 19 | } 20 | 21 | if (timeout == 1000) { 22 | notify({ type: 'error', message: `Tx timed-out. Try again` }); 23 | } 24 | return 25 | } -------------------------------------------------------------------------------- /src/stores/useUserSOLBalanceStore.tsx: -------------------------------------------------------------------------------- 1 | import create, { State } from 'zustand' 2 | import { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js' 3 | 4 | interface UserSOLBalanceStore extends State { 5 | balance: number; 6 | getUserSOLBalance: (publicKey: PublicKey, connection: Connection) => void 7 | } 8 | 9 | const useUserSOLBalanceStore = create((set, _get) => ({ 10 | balance: 0, 11 | getUserSOLBalance: async (publicKey, connection) => { 12 | let balance = 0; 13 | try { 14 | balance = await connection.getBalance( 15 | publicKey, 16 | 'confirmed' 17 | ); 18 | balance = balance / LAMPORTS_PER_SOL; 19 | } catch (e) { 20 | console.log(`error getting balance: `, e); 21 | } 22 | set((s) => { 23 | s.balance = balance; 24 | console.log(`balance updated, `, balance); 25 | }) 26 | }, 27 | })); 28 | 29 | export default useUserSOLBalanceStore; -------------------------------------------------------------------------------- /src/components/NetworkSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import dynamic from 'next/dynamic'; 3 | import { useNetworkConfiguration } from '../contexts/NetworkConfigurationProvider'; 4 | 5 | const NetworkSwitcher: FC = () => { 6 | const { networkConfiguration, setNetworkConfiguration } = useNetworkConfiguration(); 7 | 8 | console.log(networkConfiguration); 9 | 10 | return ( 11 | 23 | ); 24 | }; 25 | 26 | export default dynamic(() => Promise.resolve(NetworkSwitcher), { 27 | ssr: false 28 | }) -------------------------------------------------------------------------------- /src/contexts/NetworkConfigurationProvider.tsx: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from '@solana/wallet-adapter-react'; 2 | import { createContext, FC, ReactNode, useContext } from 'react'; 3 | 4 | 5 | export interface NetworkConfigurationState { 6 | networkConfiguration: string; 7 | setNetworkConfiguration(networkConfiguration: string): void; 8 | } 9 | 10 | export const NetworkConfigurationContext = createContext({} as NetworkConfigurationState); 11 | 12 | export function useNetworkConfiguration(): NetworkConfigurationState { 13 | return useContext(NetworkConfigurationContext); 14 | } 15 | 16 | export const NetworkConfigurationProvider: FC<{ children: ReactNode }> = ({ children }) => { 17 | const [networkConfiguration, setNetworkConfiguration] = useLocalStorage("network", "devnet"); 18 | 19 | return ( 20 | {children} 21 | ); 22 | }; -------------------------------------------------------------------------------- /src/utils/index.tsx: -------------------------------------------------------------------------------- 1 | import { format } from 'date-fns'; 2 | 3 | // Concatenates classes into a single className string 4 | const cn = (...args: string[]) => args.join(' '); 5 | 6 | const formatDate = (date: string) => format(new Date(date), 'MM/dd/yyyy h:mm:ss'); 7 | 8 | /** 9 | * Formats number as currency string. 10 | * 11 | * @param number Number to format. 12 | */ 13 | const numberToCurrencyString = (number: number) => 14 | number.toLocaleString('en-US'); 15 | 16 | /** 17 | * Returns a number whose value is limited to the given range. 18 | * 19 | * Example: limit the output of this computation to between 0 and 255 20 | * (x * 255).clamp(0, 255) 21 | * 22 | * @param {Number} min The lower boundary of the output range 23 | * @param {Number} max The upper boundary of the output range 24 | * @returns A number in the range [min, max] 25 | * @type Number 26 | */ 27 | const clamp = (current, min, max) => Math.min(Math.max(current, min), max); 28 | 29 | export { 30 | cn, 31 | formatDate, 32 | numberToCurrencyString, 33 | clamp, 34 | }; 35 | -------------------------------------------------------------------------------- /src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | type Props = { 4 | noText?: boolean; 5 | text?: string; 6 | }; 7 | export const Loader: FC = ({ text = "Loading...", noText = false }) => { 8 | return ( 9 |
10 | 16 | 24 | 29 | {" "} 30 | {!noText ?
{text}
: null} 31 |
32 | ); 33 | }; -------------------------------------------------------------------------------- /src/contexts/AutoConnectProvider.tsx: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from '@solana/wallet-adapter-react'; 2 | import { createContext, FC, ReactNode, useContext } from 'react'; 3 | 4 | export interface AutoConnectContextState { 5 | autoConnect: boolean; 6 | setAutoConnect(autoConnect: boolean): void; 7 | } 8 | 9 | export const AutoConnectContext = createContext({} as AutoConnectContextState); 10 | 11 | export function useAutoConnect(): AutoConnectContextState { 12 | return useContext(AutoConnectContext); 13 | } 14 | 15 | export const AutoConnectProvider: FC<{ children: ReactNode }> = ({ children }) => { 16 | // TODO: fix auto connect to actual reconnect on refresh/other. 17 | // TODO: make switch/slider settings 18 | // const [autoConnect, setAutoConnect] = useLocalStorage('autoConnect', false); 19 | const [autoConnect, setAutoConnect] = useLocalStorage('autoConnect', true); 20 | 21 | return ( 22 | {children} 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from 'next/app'; 2 | import Head from 'next/head'; 3 | import { FC } from 'react'; 4 | import { ContextProvider } from '../contexts/ContextProvider'; 5 | import { AppBar } from '../components/AppBar'; 6 | import { ContentContainer } from '../components/ContentContainer'; 7 | import { Footer } from '../components/Footer'; 8 | import Notifications from '../components/Notification' 9 | require('@solana/wallet-adapter-react-ui/styles.css'); 10 | require('../styles/globals.css'); 11 | 12 | const App: FC = ({ Component, pageProps }) => { 13 | return ( 14 | <> 15 | 16 | Solana-Tools 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 |
26 | 27 |
28 |
29 | 30 | ); 31 | }; 32 | 33 | export default App; 34 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import Link from 'next/link'; 3 | import { GithubIcon, TwitterIcon } from 'lucide-react'; 4 | export const Footer: FC = () => { 5 | return ( 6 |
7 |
8 |
9 |
10 | 11 | 12 |
Github
13 | 14 | 15 | 16 |
ToolsSolana
17 | 18 | 19 | 20 |
LaLoutre
21 | 22 | 23 |
24 |
25 |
26 |
27 | ); 28 | }; -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { DocumentContext, Head, Html, Main, NextScript } from 'next/document' 2 | 3 | class MyDocument extends Document { 4 | static async getInitialProps(ctx: DocumentContext) { 5 | const initialProps = await Document.getInitialProps(ctx) 6 | 7 | return initialProps 8 | } 9 | 10 | render() { 11 | return ( 12 | 13 | 14 | 18 | 23 | 29 | 35 | 36 | 37 | 38 | 39 |
40 | 41 | 42 | 43 | ); 44 | } 45 | } 46 | 47 | export default MyDocument; 48 | -------------------------------------------------------------------------------- /src/utils/getNonEmptyAccounts.tsx: -------------------------------------------------------------------------------- 1 | import { getTransferFeeAmount, unpackAccount } from "@solana/spl-token"; 2 | import { PublicKey, Connection } from "@solana/web3.js"; 3 | 4 | export async function getNonEmptyTokenAccounts(owner: PublicKey, connection: Connection, program: PublicKey) { 5 | 6 | const nonEmptyAccounts: { account: PublicKey; program: PublicKey, lamports: number, mint: string, amount: number, hasWithheldAmount: boolean }[] = []; 7 | const accounts = (await connection.getTokenAccountsByOwner(owner, { programId: program })).value; 8 | 9 | accounts.map((account) => { 10 | const mintBuffer = account.account.data.slice(0, 32); 11 | const mint = new PublicKey(mintBuffer); 12 | const amount = account.account.data.readBigInt64LE(64); 13 | const unpackedAccount = unpackAccount(account.pubkey, account.account, program); 14 | const transferFeeAmount = getTransferFeeAmount(unpackedAccount); 15 | let hasWithheldAmount = false; 16 | if ( 17 | transferFeeAmount != null && 18 | transferFeeAmount.withheldAmount > BigInt(0) 19 | ) { 20 | hasWithheldAmount = true; 21 | } 22 | if (amount != BigInt(0)) { 23 | nonEmptyAccounts.push({ 24 | mint: mint.toBase58(), 25 | account: account.pubkey, 26 | amount: Number(amount), 27 | program: program, 28 | lamports: account.account.lamports, 29 | hasWithheldAmount: hasWithheldAmount 30 | }); 31 | } 32 | }) 33 | 34 | return nonEmptyAccounts; 35 | } -------------------------------------------------------------------------------- /src/utils/getEmptyAccounts.tsx: -------------------------------------------------------------------------------- 1 | import { getTransferFeeAmount, unpackAccount } from "@solana/spl-token"; 2 | import { PublicKey, Connection } from "@solana/web3.js"; 3 | 4 | export async function getEmptyTokenAccounts( 5 | owner: PublicKey, 6 | connection: Connection, 7 | program: PublicKey, 8 | ) { 9 | const emptyTokenAccounts: { account: PublicKey; program: PublicKey, lamports: number, mint: string, amount: number, hasWithheldAmount: boolean }[] = []; 10 | const accounts = ( 11 | await connection.getTokenAccountsByOwner(owner, { programId: program }, { commitment: "confirmed" }) 12 | ).value; 13 | 14 | accounts.map((account) => { 15 | if (account.account.data.readBigInt64LE(64) == BigInt(0)) { 16 | const mintBuffer = account.account.data.slice(0, 32); 17 | const mint = new PublicKey(mintBuffer); 18 | const unpackedAccount = unpackAccount(account.pubkey, account.account, program); 19 | const transferFeeAmount = getTransferFeeAmount(unpackedAccount); 20 | let hasWithheldAmount = false; 21 | if ( 22 | transferFeeAmount != null && 23 | transferFeeAmount.withheldAmount > BigInt(0) 24 | ) { 25 | hasWithheldAmount = true; 26 | } 27 | emptyTokenAccounts.push({ 28 | account: account.pubkey, 29 | program: program, 30 | lamports: account.account.lamports, 31 | mint: mint.toBase58(), 32 | amount: 0, 33 | hasWithheldAmount: hasWithheldAmount 34 | }); 35 | } 36 | }); 37 | 38 | return emptyTokenAccounts; 39 | } -------------------------------------------------------------------------------- /src/components/ContentContainer.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import Link from "next/link"; 3 | import Text from './Text'; 4 | import NavElement from './nav-element'; 5 | interface Props { 6 | children: React.ReactNode; 7 | } 8 | 9 | export const ContentContainer: React.FC = ({ children }) => { 10 | 11 | return ( 12 |
13 | 14 |
15 | {children} 16 |
17 | {/* SideBar / Drawer */} 18 |
19 | 20 | 21 |
    22 |
  • 23 | Menu 24 |
  • 25 |
  • 26 | 30 |
  • 31 |
  • 32 | 36 | 40 | 44 | 48 |
  • 49 |
50 |
51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/components/Text/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import React from 'react'; 3 | import { cn } from 'utils'; 4 | 5 | /** 6 | * Properties for a card component. 7 | */ 8 | type TextProps = { 9 | variant: 10 | | 'big-heading' 11 | | 'heading' 12 | | 'sub-heading' 13 | | 'nav-heading' 14 | | 'nav' 15 | | 'input' 16 | | 'label'; 17 | className?: string; 18 | href?: string; 19 | children?: React.ReactNode; 20 | id?: string; 21 | }; 22 | 23 | /** 24 | * Pre-defined styling, according to agreed-upon design-system. 25 | */ 26 | const variants = { 27 | heading: 'text-3xl font-medium', 28 | 'sub-heading': 'text-2xl font-medium', 29 | 'nav-heading': 'text-lg font-medium sm:text-xl', 30 | nav: 'font-medium', 31 | paragraph: 'text-lg', 32 | 'sub-paragraph': 'text-base font-medium text-inherit', 33 | input: 'text-sm uppercase tracking-wide', 34 | label: 'text-xs uppercase tracking-wide', 35 | }; 36 | 37 | /** 38 | * Definition of a card component,the main purpose of 39 | * which is to neatly display information. Can be both 40 | * interactive and static. 41 | * 42 | * @param variant Variations relating to pre-defined styling of the element. 43 | * @param className Custom classes to be applied to the element. 44 | * @param children Child elements to be rendered within the component. 45 | */ 46 | const Text = ({ variant, className, href, children }: TextProps) => ( 47 |

48 | {href ? ( 49 | 50 | {children} 51 | 52 | ) : ( 53 | children 54 | )} 55 |

56 | ); 57 | 58 | export default Text; -------------------------------------------------------------------------------- /src/utils/filterByScamsAndLegit.tsx: -------------------------------------------------------------------------------- 1 | import { PublicKey, Connection } from "@solana/web3.js"; 2 | import { SCAM_TOKEN_LIST } from "./scamToken"; 3 | import { TokenStandard } from "@metaplex-foundation/mpl-token-metadata"; 4 | import { Pda } from "@metaplex-foundation/umi"; 5 | 6 | export function filterByScamsAndLegit( 7 | assets: { 8 | account: PublicKey; 9 | program: PublicKey; 10 | lamports: number; 11 | mint: string; 12 | name: string; 13 | image: string; 14 | amount: number; 15 | hasWithheldAmount: boolean; 16 | tokenStandard: TokenStandard; 17 | collectionMetadata: Pda | undefined; 18 | tokenRecord: Pda | undefined 19 | }[] 20 | ) { 21 | const scamAssets: { 22 | account: PublicKey; 23 | program: PublicKey; 24 | lamports: number; 25 | mint: string; 26 | name: string; 27 | image: string; 28 | amount: number; 29 | hasWithheldAmount: boolean; 30 | tokenStandard: TokenStandard; 31 | collectionMetadata: Pda | undefined; 32 | tokenRecord: Pda | undefined 33 | }[] = []; 34 | const legitAssets: { 35 | account: PublicKey; 36 | program: PublicKey; 37 | lamports: number; 38 | mint: string; 39 | name: string; 40 | image: string; 41 | amount: number; 42 | hasWithheldAmount: boolean; 43 | tokenStandard: TokenStandard; 44 | collectionMetadata: Pda | undefined; 45 | tokenRecord: Pda | undefined 46 | }[] = []; 47 | 48 | assets.map((asset) => { 49 | if (SCAM_TOKEN_LIST.includes(asset.mint)) { 50 | scamAssets.push(asset) 51 | } 52 | else { 53 | legitAssets.push(asset) 54 | } 55 | }) 56 | 57 | return [scamAssets, legitAssets]; 58 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solana-dapp-next", 3 | "version": "0.2.0", 4 | "author": "Immutal0", 5 | "license": "MIT", 6 | "private": false, 7 | "scripts": { 8 | "dev": "next dev", 9 | "build": "next build", 10 | "start": "next start", 11 | "lint": "next lint" 12 | }, 13 | "dependencies": { 14 | "@heroicons/react": "^1.0.5", 15 | "@metaplex-foundation/mpl-token-metadata": "^3.3.0", 16 | "@metaplex-foundation/mpl-toolbox": "^0.9.4", 17 | "@metaplex-foundation/umi": "^0.9.2", 18 | "@metaplex-foundation/umi-bundle-defaults": "^0.9.2", 19 | "@metaplex-foundation/umi-signer-wallet-adapters": "^0.9.2", 20 | "@metaplex-foundation/umi-uploader-irys": "^0.10.0-beta.0", 21 | "@noble/ed25519": "^1.7.1", 22 | "@solana/spl-token": "^0.4.9", 23 | "@solana/wallet-adapter-base": "^0.9.23", 24 | "@solana/wallet-adapter-react": "^0.15.35", 25 | "@solana/wallet-adapter-react-ui": "^0.9.35", 26 | "@solana/wallet-adapter-wallets": "^0.19.32", 27 | "@solana/web3.js": "^1.95.4", 28 | "@tailwindcss/typography": "^0.5.9", 29 | "bs58": "^6.0.0", 30 | "clsx": "^2.1.1", 31 | "daisyui": "^1.24.3", 32 | "date-fns": "^2.29.3", 33 | "helius-sdk": "^1.4.0", 34 | "immer": "^9.0.12", 35 | "lucide-react": "^0.454.0", 36 | "next": "^13.1.5", 37 | "next-compose-plugins": "^2.2.1", 38 | "next-transpile-modules": "^10.0.0", 39 | "react": "^18.2.0", 40 | "react-dom": "^18.2.0", 41 | "tailwind-merge": "^2.5.4", 42 | "zustand": "^3.6.9" 43 | }, 44 | "devDependencies": { 45 | "@types/node": "^18.11.18", 46 | "@types/react": "^18.0.27", 47 | "autoprefixer": "^10.4.2", 48 | "eslint": "8.7.0", 49 | "eslint-config-next": "^13.1.5", 50 | "postcss": "^8.4.5", 51 | "tailwindcss": "^3.2.4", 52 | "typescript": "^5.6.3" 53 | }, 54 | "engines": { 55 | "node": ">=16" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/components/nav-element/index.tsx: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-empty */ 2 | import Link from 'next/link'; 3 | import Text from '../Text'; 4 | import { cn } from '../../utils'; 5 | import { useRouter } from 'next/router'; 6 | import { useEffect, useRef } from 'react'; 7 | 8 | type NavElementProps = { 9 | label: string; 10 | href: string; 11 | as?: string; 12 | scroll?: boolean; 13 | chipLabel?: string; 14 | disabled?: boolean; 15 | navigationStarts?: () => void; 16 | }; 17 | 18 | const NavElement = ({ 19 | label, 20 | href, 21 | as, 22 | scroll, 23 | disabled, 24 | navigationStarts = () => {}, 25 | }: NavElementProps) => { 26 | const router = useRouter(); 27 | const isActive = href === router.asPath || (as && as === router.asPath); 28 | const divRef = useRef(null); 29 | 30 | useEffect(() => { 31 | if (divRef.current) { 32 | divRef.current.className = cn( 33 | 'h-0.5 w-1/4 transition-all duration-300 ease-out', 34 | isActive 35 | ? '!w-full bg-gradient-to-l from-fuchsia-500 to-pink-500 ' 36 | : 'group-hover:w-1/2 group-hover:bg-fuchsia-500', 37 | ); 38 | } 39 | }, [isActive]); 40 | 41 | return ( 42 | navigationStarts()} 53 | > 54 |
55 | {label} 56 |
57 |
58 | 59 | ); 60 | }; 61 | 62 | export default NavElement; 63 | 64 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )); 45 | CardTitle.displayName = "CardTitle"; 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )); 57 | CardDescription.displayName = "CardDescription"; 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )); 65 | CardContent.displayName = "CardContent"; 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )); 77 | CardFooter.displayName = "CardFooter"; 78 | 79 | export { 80 | Card, 81 | CardHeader, 82 | CardFooter, 83 | CardTitle, 84 | CardDescription, 85 | CardContent, 86 | }; -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: "jit", 3 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 4 | darkMode: "media", 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [ 9 | require('daisyui'), 10 | require("@tailwindcss/typography") 11 | ], 12 | daisyui: { 13 | styled: true, 14 | // TODO: Theme needs works 15 | themes: [ 16 | { 17 | 'solana': { 18 | fontFamily: { 19 | display: ['PT Mono, monospace'], 20 | body: ['Inter, sans-serif'], 21 | }, 22 | 'primary': '#000000', /* Primary color */ 23 | 'primary-focus': '#9945FF', /* Primary color - focused */ 24 | 'primary-content': '#ffffff', /* Foreground content color to use on primary color */ 25 | 26 | 'secondary': '#808080', /* Secondary color */ 27 | 'secondary-focus': '#f3cc30', /* Secondary color - focused */ 28 | 'secondary-content': '#ffffff', /* Foreground content color to use on secondary color */ 29 | 30 | 'accent': '#33a382', /* Accent color */ 31 | 'accent-focus': '#2aa79b', /* Accent color - focused */ 32 | 'accent-content': '#ffffff', /* Foreground content color to use on accent color */ 33 | 34 | 'neutral': '#2b2b2b', /* Neutral color */ 35 | 'neutral-focus': '#2a2e37', /* Neutral color - focused */ 36 | 'neutral-content': '#ffffff', /* Foreground content color to use on neutral color */ 37 | 38 | 'base-100': '#000000', /* Base color of page, used for blank backgrounds */ 39 | 'base-200': '#35363a', /* Base color, a little darker */ 40 | 'base-300': '#222222', /* Base color, even more darker */ 41 | 'base-content': '#f9fafb', /* Foreground content color to use on base color */ 42 | 43 | 'info': '#2094f3', /* Info */ 44 | 'success': '#009485', /* Success */ 45 | 'warning': '#ff9900', /* Warning */ 46 | 'error': '#ff5724', /* Error */ 47 | }, 48 | }, 49 | // backup themes: 50 | // 'dark', 51 | // 'synthwave' 52 | ], 53 | base: true, 54 | utils: true, 55 | logs: true, 56 | rtl: false, 57 | }, 58 | } -------------------------------------------------------------------------------- /src/contexts/ContextProvider.tsx: -------------------------------------------------------------------------------- 1 | import { WalletAdapterNetwork, WalletError } from '@solana/wallet-adapter-base'; 2 | import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react'; 3 | import { 4 | UnsafeBurnerWalletAdapter 5 | } from '@solana/wallet-adapter-wallets'; 6 | import { Cluster, clusterApiUrl } from '@solana/web3.js'; 7 | import { FC, ReactNode, useCallback, useMemo } from 'react'; 8 | import { AutoConnectProvider, useAutoConnect } from './AutoConnectProvider'; 9 | import { notify } from "../utils/notifications"; 10 | import { NetworkConfigurationProvider, useNetworkConfiguration } from './NetworkConfigurationProvider'; 11 | import dynamic from "next/dynamic"; 12 | 13 | const ReactUIWalletModalProviderDynamic = dynamic( 14 | async () => 15 | (await import("@solana/wallet-adapter-react-ui")).WalletModalProvider, 16 | { ssr: false } 17 | ); 18 | 19 | const WalletContextProvider: FC<{ children: ReactNode }> = ({ children }) => { 20 | const { autoConnect } = useAutoConnect(); 21 | const { networkConfiguration } = useNetworkConfiguration(); 22 | const network = networkConfiguration as WalletAdapterNetwork; 23 | const endpoint = useMemo(() => clusterApiUrl(network), [network]); 24 | 25 | console.log(network); 26 | 27 | const wallets = useMemo( 28 | () => [ 29 | new UnsafeBurnerWalletAdapter(), 30 | ], 31 | [network] 32 | ); 33 | 34 | const onError = useCallback( 35 | (error: WalletError) => { 36 | notify({ type: 'error', message: error.message ? `${error.name}: ${error.message}` : error.name }); 37 | console.error(error); 38 | }, 39 | [] 40 | ); 41 | 42 | return ( 43 | // TODO: updates needed for updating and referencing endpoint: wallet adapter rework 44 | 45 | 46 | 47 | {children} 48 | 49 | 50 | 51 | ); 52 | }; 53 | 54 | export const ContextProvider: FC<{ children: ReactNode }> = ({ children }) => { 55 | return ( 56 | <> 57 | 58 | 59 | {children} 60 | 61 | 62 | 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /src/components/SignMessage.tsx: -------------------------------------------------------------------------------- 1 | // TODO: SignMessage 2 | import { verify } from '@noble/ed25519'; 3 | import { useWallet } from '@solana/wallet-adapter-react'; 4 | import bs58 from 'bs58'; 5 | import { FC, useCallback } from 'react'; 6 | import { notify } from "../utils/notifications"; 7 | 8 | export const SignMessage: FC = () => { 9 | const { publicKey, signMessage } = useWallet(); 10 | 11 | const onClick = useCallback(async () => { 12 | try { 13 | // `publicKey` will be null if the wallet isn't connected 14 | if (!publicKey) throw new Error('Wallet not connected!'); 15 | // `signMessage` will be undefined if the wallet doesn't support it 16 | if (!signMessage) throw new Error('Wallet does not support message signing!'); 17 | // Encode anything as bytes 18 | const message = new TextEncoder().encode('Hello, world!'); 19 | // Sign the bytes using the wallet 20 | const signature = await signMessage(message); 21 | // Verify that the bytes were signed using the private key that matches the known public key 22 | if (!verify(signature, message, publicKey.toBytes())) throw new Error('Invalid signature!'); 23 | notify({ type: 'success', message: 'Sign message successful!', txid: bs58.encode(signature) }); 24 | } catch (error: any) { 25 | notify({ type: 'error', message: `Sign Message failed!`, description: error?.message }); 26 | console.log('error', `Sign Message failed! ${error?.message}`); 27 | } 28 | }, [publicKey, notify, signMessage]); 29 | 30 | return ( 31 |
32 |
33 |
35 | 46 |
47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/Dropdown/NFTDropdown.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | import React, { 3 | useCallback, 4 | useEffect, 5 | useRef, 6 | useState, 7 | } from "react"; 8 | import Link from "next/link"; 9 | 10 | const DropDownItems: Array<{ 11 | title: string; 12 | href: string; 13 | }> = [ 14 | // { 15 | // title: "Create", 16 | // href: "/create", 17 | // }, 18 | { 19 | title: "Burn", 20 | href: "/burn", 21 | }, 22 | { 23 | title: "Close", 24 | href: "/close", 25 | } 26 | ]; 27 | 28 | export const NFTDropdown: FC = () => { 29 | const [active, setActive] = useState(false); 30 | const ref = useRef(null); 31 | 32 | const openDropdown = useCallback(() => { 33 | setActive(true); 34 | }, []); 35 | 36 | const closeDropdown = useCallback(() => { 37 | setActive(false); 38 | }, []); 39 | 40 | useEffect(() => { 41 | const listener = (event: MouseEvent | TouchEvent) => { 42 | const node = ref.current; 43 | 44 | // Do nothing if clicking dropdown or its descendants 45 | if (!node || node.contains(event.target as Node)) return; 46 | 47 | closeDropdown(); 48 | }; 49 | 50 | document.addEventListener("mousedown", listener); 51 | document.addEventListener("touchstart", listener); 52 | 53 | return () => { 54 | document.removeEventListener("mousedown", listener); 55 | document.removeEventListener("touchstart", listener); 56 | }; 57 | }, [ref, closeDropdown]); 58 | 59 | return ( 60 |
61 | 83 |
    90 | {DropDownItems.map((item, key) => { 91 | return ( 92 |
  • 93 | {item.title} 94 |
  • 95 | ) 96 | })} 97 |
98 |
99 | ); 100 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana-Tools 2 | 3 | A bunch of tools to help people in the Solana ecosystem. This website includes: 4 | - a UI to burn Solana NFTs 5 | - a UI to burn SPL-tokens 6 | - a UI to close empty accounts 7 | - a multi sender (a UI to send multiple tokens in 1 transaction (same token to different people/many tokens to one person/transfer solana domain name) 8 | - a UI to create SPL-Tokens 9 | - a UI to upload file to Arweave 10 | - a UI to update the metadata of your NFT 11 | - a UI to send a NFT message to the owner of your desired NFT or solana domain name 12 | - More tools are scheduled... 13 | 14 | ## Getting Started 15 | 16 | Clone the repo, install the dependencies and run `yarn run dev` to run the development server. 17 | 18 | ```bash 19 | git clone https://github.com/Immutal0/solana-tools.git 20 | cd solana-tools 21 | yarn install 22 | yarn run dev 23 | ``` 24 | 25 | 26 | ## Burn NFT UI 27 | A UI for burning Solana NFTs and getting back $SOL from the associated token account. 28 | 29 | ## Burn SPL-tokens UI 30 | A UI for burning SPL-tokens and getting back $SOL from the associated token account. 31 | 32 | ## Close empty account UI 33 | A UI to close empty token account and getting back $SOL from the associated token account. 34 | 35 | ## Multi sender UI 36 | A UI to send multiple tokens in 1 transaction (same token to different people/many tokens to one person/transfer solana domain name) 37 | 38 | ## Create SPL-Tokens UI 39 | An UI to create SPL-Tokens with one click. 40 | 41 | ## Upload File 42 | An UI to upload file to Arweave. 43 | 44 | ## Update NFT metadata UI 45 | An UI to update the metadata of your NFT 46 | 47 | ## Send NFT message 48 | An UI to send a NFT message to the owner of your desired NFT 49 | 50 | ## Style 51 | 52 | [Tailwind CSS](https://tailwindcss.com/) or [daisyUI](https://daisyui.com/) are selected tools for rapid style development. 53 | 54 | You can quickly change theme changing `daisy.themes` within `./tailwind.config.js`. 55 | More info here: https://daisyui.com/docs/default-themes 56 | 57 | This app encourages you to use CSS Modules over other style techniques (like SASS/LESS, Styled Components, usual CSS). 58 | It has a modular nature and supports modern CSS. [Read more on Next.JS site](https://nextjs.org/docs/basic-features/built-in-css-support). 59 | Anyway, if you want to connect LESS there is example code in `./next.config.js` 60 | 61 | ## Deploy on Vercel 62 | 63 | Before push run locally `npm run build` to make sure app can be build successfully on vercel. 64 | 65 | Vercel will automatically create environment and deployment for you if you have vercel account connected to your GitHub account. Go to the vercel.com to connect it. 66 | Then any push to `main` branch will automatically rebuild and redeploy app. 67 | 68 | To deploy on Vercel use the following settings : 69 | 70 |

71 | 72 |

73 | 74 | 75 | ## Community 76 | If you have questions or any troubles, feel free to reach me on 77 | X [@Immutal0_](https://x.com/Immutal0_) and Telegram [@frankeindev](https://t.me/frankeindev) 78 | -------------------------------------------------------------------------------- /src/components/Dropdown/TokenDropdown.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | import React, { 3 | useCallback, 4 | useEffect, 5 | useRef, 6 | useState, 7 | } from "react"; 8 | import Link from "next/link"; 9 | 10 | const DropDownItems: Array<{ 11 | title: string; 12 | href: string; 13 | }> = [ 14 | { 15 | title: "Create", 16 | href: "/create", 17 | }, 18 | { 19 | title: "Burn", 20 | href: "/burn", 21 | }, 22 | { 23 | title: "Close", 24 | href: "/close", 25 | }, 26 | { 27 | title: "Claim Transfer Fees", 28 | href: "/claim-fees", 29 | } 30 | ]; 31 | 32 | export const TokenDropdown: FC = () => { 33 | const [active, setActive] = useState(false); 34 | const ref = useRef(null); 35 | 36 | const openDropdown = useCallback(() => { 37 | setActive(true); 38 | }, []); 39 | 40 | const closeDropdown = useCallback(() => { 41 | setActive(false); 42 | }, []); 43 | 44 | useEffect(() => { 45 | const listener = (event: MouseEvent | TouchEvent) => { 46 | const node = ref.current; 47 | 48 | // Do nothing if clicking dropdown or its descendants 49 | if (!node || node.contains(event.target as Node)) return; 50 | 51 | closeDropdown(); 52 | }; 53 | 54 | document.addEventListener("mousedown", listener); 55 | document.addEventListener("touchstart", listener); 56 | 57 | return () => { 58 | document.removeEventListener("mousedown", listener); 59 | document.removeEventListener("touchstart", listener); 60 | }; 61 | }, [ref, closeDropdown]); 62 | 63 | return ( 64 |
65 | 87 |
    94 | {DropDownItems.map((item, key) => { 95 | return ( 96 |
  • 97 | {item.title} 98 |
  • 99 | ) 100 | })} 101 |
102 |
103 | ); 104 | }; -------------------------------------------------------------------------------- /src/utils/getCloseTransactions.tsx: -------------------------------------------------------------------------------- 1 | import { ComputeBudgetProgram, Connection, PublicKey, Transaction } from "@solana/web3.js"; 2 | import { createCloseAccountInstruction, createHarvestWithheldTokensToMintInstruction } from "@solana/spl-token"; 3 | import { AUTHORITY } from "config"; 4 | import { CLOSE_ACCOUNT_CU, ADD_COMPUTE_UNIT_PRICE_CU, ADD_COMPUTE_UNIT_LIMIT_CU, HARVEST_TOKENS_CU } from "./CUPerInstruction"; 5 | import { Pda } from "@metaplex-foundation/umi"; 6 | import { TokenStandard } from "@metaplex-foundation/mpl-token-metadata"; 7 | 8 | export async function getCloseTransactions( 9 | assets: { 10 | account: PublicKey; 11 | program: PublicKey; 12 | image: string; 13 | name: string; 14 | mint: string; 15 | lamports: number; 16 | amount: number; 17 | hasWithheldAmount: boolean; 18 | tokenStandard: TokenStandard; 19 | collectionMetadata: Pda | undefined; 20 | tokenRecord: Pda | undefined; 21 | }[], 22 | connection: Connection, 23 | publicKey: PublicKey) { 24 | 25 | const transactions: Transaction[] = []; 26 | const nbPerTx = 20; 27 | 28 | let nbTx: number; 29 | if (assets.length % nbPerTx == 0) { 30 | nbTx = assets.length / nbPerTx; 31 | } else { 32 | nbTx = Math.floor(assets.length / nbPerTx) + 1; 33 | } 34 | 35 | for (let i = 0; i < nbTx; i++) { 36 | let bornSup: number; 37 | 38 | if (i == nbTx - 1) { 39 | bornSup = assets.length; 40 | } else { 41 | bornSup = nbPerTx * (i + 1); 42 | } 43 | 44 | let Tx = new Transaction().add( 45 | ComputeBudgetProgram.setComputeUnitPrice({ 46 | microLamports: 1000, 47 | })); 48 | 49 | let nbCloseinstruction = 0; 50 | let nbHarvestToken = 0; 51 | for (let j = nbPerTx * i; j < bornSup; j++) { 52 | nbCloseinstruction += 1; 53 | if (assets[j].hasWithheldAmount) { 54 | nbHarvestToken += 1; 55 | Tx.add( 56 | createHarvestWithheldTokensToMintInstruction( 57 | new PublicKey(assets[j].mint), 58 | [assets[j].account], 59 | assets[j].program 60 | )) 61 | } 62 | Tx.add( 63 | createCloseAccountInstruction( 64 | assets[j].account, 65 | publicKey, 66 | publicKey, 67 | [], 68 | assets[j].program, 69 | ), 70 | ); 71 | } 72 | Tx.add( 73 | ComputeBudgetProgram.setComputeUnitLimit({ 74 | units: nbCloseinstruction * CLOSE_ACCOUNT_CU + nbHarvestToken * HARVEST_TOKENS_CU + ADD_COMPUTE_UNIT_PRICE_CU + ADD_COMPUTE_UNIT_LIMIT_CU 75 | })); 76 | 77 | const NON_MEMO_IX_INDEX = 0; 78 | 79 | // inject an authority key to track this transaction on chain 80 | Tx.instructions[NON_MEMO_IX_INDEX].keys.push({ 81 | pubkey: AUTHORITY, 82 | isWritable: false, 83 | isSigner: false, 84 | }); 85 | transactions.push(Tx); 86 | } 87 | 88 | const latestBlockhash = await connection.getLatestBlockhash(); 89 | for (let k = 0; k < transactions.length; k++) { 90 | transactions[k].recentBlockhash = latestBlockhash.blockhash; 91 | transactions[k].feePayer = publicKey; 92 | } 93 | 94 | return transactions 95 | } -------------------------------------------------------------------------------- /src/utils/getWithdrawTransactions.tsx: -------------------------------------------------------------------------------- 1 | import { ComputeBudgetProgram, Connection, PublicKey, Transaction } from "@solana/web3.js"; 2 | import { createAssociatedTokenAccountInstruction, createWithdrawWithheldTokensFromAccountsInstruction, getAssociatedTokenAddress, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; 3 | import { AUTHORITY } from "config"; 4 | import { ADD_COMPUTE_UNIT_PRICE_CU, ADD_COMPUTE_UNIT_LIMIT_CU, WITHDRAW_TRANSFER_FEES_FOR_ADDITIONAL_ACCOUNT_CU, WITHDRAW_TRANSFER_FEES_FOR_ONE_ACCOUNT_CU } from "./CUPerInstruction"; 5 | 6 | export async function getWithdrawTransactions( 7 | accounts: PublicKey[], 8 | tokenAddress: string, 9 | connection: Connection, 10 | publicKey: PublicKey) { 11 | 12 | const transactions: Transaction[] = []; 13 | const mint = new PublicKey(tokenAddress); 14 | const destinationAccount = await getAssociatedTokenAddress(mint, publicKey, undefined, TOKEN_2022_PROGRAM_ID); 15 | const info = await connection.getAccountInfo(destinationAccount); 16 | 17 | if (info == null) { 18 | const createTransaction = new Transaction().add(ComputeBudgetProgram.setComputeUnitPrice({ 19 | microLamports: 1000, 20 | }), 21 | ComputeBudgetProgram.setComputeUnitLimit({ 22 | units: 25000 + ADD_COMPUTE_UNIT_LIMIT_CU + ADD_COMPUTE_UNIT_PRICE_CU 23 | }), 24 | createAssociatedTokenAccountInstruction(publicKey, destinationAccount, publicKey, mint, TOKEN_2022_PROGRAM_ID)); 25 | transactions.push(createTransaction); 26 | } 27 | 28 | const nbWithdrawsPerTx = 25; 29 | 30 | // calculate the total number of transactions to do 31 | let nbTx: number; 32 | if (accounts.length % nbWithdrawsPerTx == 0) { 33 | nbTx = accounts.length / nbWithdrawsPerTx; 34 | } else { 35 | nbTx = Math.floor(accounts.length / nbWithdrawsPerTx) + 1; 36 | } 37 | 38 | // for each transaction 39 | for (let i = 0; i < nbTx; i++) { 40 | let bornSup: number; 41 | if (i == nbTx - 1) { 42 | bornSup = accounts.length; 43 | } else { 44 | bornSup = nbWithdrawsPerTx * (i + 1); 45 | } 46 | 47 | const start = nbWithdrawsPerTx * i; // index of the begining of the sub array 48 | const end = bornSup; // index of the end of the sub array 49 | 50 | const transaction = new Transaction(); 51 | transaction.add( 52 | ComputeBudgetProgram.setComputeUnitPrice({ 53 | microLamports: 1000, 54 | }), 55 | ComputeBudgetProgram.setComputeUnitLimit({ 56 | units: ADD_COMPUTE_UNIT_LIMIT_CU + ADD_COMPUTE_UNIT_PRICE_CU + WITHDRAW_TRANSFER_FEES_FOR_ONE_ACCOUNT_CU + (end - start) * WITHDRAW_TRANSFER_FEES_FOR_ADDITIONAL_ACCOUNT_CU 57 | }), 58 | createWithdrawWithheldTokensFromAccountsInstruction(mint, destinationAccount, publicKey, [], accounts.slice(start, end), TOKEN_2022_PROGRAM_ID) 59 | ); 60 | 61 | const NON_MEMO_IX_INDEX = 0; 62 | 63 | // inject an authority key to track this transaction on chain 64 | transaction.instructions[NON_MEMO_IX_INDEX].keys.push({ 65 | pubkey: AUTHORITY, 66 | isWritable: false, 67 | isSigner: false, 68 | }); 69 | transactions.push(transaction); 70 | } 71 | 72 | const latestBlockhash = await connection.getLatestBlockhash(); 73 | for (let k = 0; k < transactions.length; k++) { 74 | transactions[k].recentBlockhash = latestBlockhash.blockhash; 75 | transactions[k].feePayer = publicKey; 76 | } 77 | 78 | return transactions 79 | } -------------------------------------------------------------------------------- /src/components/SendVersionedTransaction.tsx: -------------------------------------------------------------------------------- 1 | import { useConnection, useWallet } from '@solana/wallet-adapter-react'; 2 | import { Keypair, SystemProgram, TransactionMessage, TransactionSignature, VersionedTransaction } from '@solana/web3.js'; 3 | import { FC, useCallback } from 'react'; 4 | import { notify } from "../utils/notifications"; 5 | 6 | export const SendVersionedTransaction: FC = () => { 7 | const { connection } = useConnection(); 8 | const { publicKey, sendTransaction } = useWallet(); 9 | 10 | const onClick = useCallback(async () => { 11 | if (!publicKey) { 12 | notify({ type: 'error', message: `Wallet not connected!` }); 13 | console.log('error', `Send Transaction: Wallet not connected!`); 14 | return; 15 | } 16 | 17 | let signature: TransactionSignature = ''; 18 | try { 19 | 20 | // Create instructions to send, in this case a simple transfer 21 | const instructions = [ 22 | SystemProgram.transfer({ 23 | fromPubkey: publicKey, 24 | toPubkey: Keypair.generate().publicKey, 25 | lamports: 1_000_000, 26 | }), 27 | ]; 28 | 29 | // Get the lates block hash to use on our transaction and confirmation 30 | let latestBlockhash = await connection.getLatestBlockhash() 31 | 32 | // Create a new TransactionMessage with version and compile it to version 0 33 | const messageV0 = new TransactionMessage({ 34 | payerKey: publicKey, 35 | recentBlockhash: latestBlockhash.blockhash, 36 | instructions, 37 | }).compileToV0Message(); 38 | 39 | // Create a new VersionedTransacction to support the v0 message 40 | const transation = new VersionedTransaction(messageV0) 41 | 42 | // Send transaction and await for signature 43 | signature = await sendTransaction(transation, connection); 44 | 45 | // Await for confirmation 46 | await connection.confirmTransaction({ signature, ...latestBlockhash }, 'confirmed'); 47 | 48 | console.log(signature); 49 | notify({ type: 'success', message: 'Transaction successful!', txid: signature }); 50 | } catch (error: any) { 51 | notify({ type: 'error', message: `Transaction failed!`, description: error?.message, txid: signature }); 52 | console.log('error', `Transaction failed! ${error?.message}`, signature); 53 | return; 54 | } 55 | }, [publicKey, notify, connection, sendTransaction]); 56 | 57 | return ( 58 |
59 |
60 |
62 | 73 |
74 |
75 | ); 76 | }; -------------------------------------------------------------------------------- /src/components/SendTransaction.tsx: -------------------------------------------------------------------------------- 1 | import { useConnection, useWallet } from '@solana/wallet-adapter-react'; 2 | import { Keypair, SystemProgram, Transaction, TransactionMessage, TransactionSignature, VersionedTransaction } from '@solana/web3.js'; 3 | import { FC, useCallback } from 'react'; 4 | import { notify } from "../utils/notifications"; 5 | 6 | export const SendTransaction: FC = () => { 7 | const { connection } = useConnection(); 8 | const { publicKey, sendTransaction } = useWallet(); 9 | 10 | const onClick = useCallback(async () => { 11 | if (!publicKey) { 12 | notify({ type: 'error', message: `Wallet not connected!` }); 13 | console.log('error', `Send Transaction: Wallet not connected!`); 14 | return; 15 | } 16 | 17 | let signature: TransactionSignature = ''; 18 | try { 19 | 20 | // Create instructions to send, in this case a simple transfer 21 | const instructions = [ 22 | SystemProgram.transfer({ 23 | fromPubkey: publicKey, 24 | toPubkey: Keypair.generate().publicKey, 25 | lamports: 1_000_000, 26 | }), 27 | ]; 28 | 29 | // Get the lates block hash to use on our transaction and confirmation 30 | let latestBlockhash = await connection.getLatestBlockhash() 31 | 32 | // Create a new TransactionMessage with version and compile it to legacy 33 | const messageLegacy = new TransactionMessage({ 34 | payerKey: publicKey, 35 | recentBlockhash: latestBlockhash.blockhash, 36 | instructions, 37 | }).compileToLegacyMessage(); 38 | 39 | // Create a new VersionedTransacction which supports legacy and v0 40 | const transation = new VersionedTransaction(messageLegacy) 41 | 42 | // Send transaction and await for signature 43 | signature = await sendTransaction(transation, connection); 44 | 45 | // Send transaction and await for signature 46 | await connection.confirmTransaction({ signature, ...latestBlockhash }, 'confirmed'); 47 | 48 | console.log(signature); 49 | notify({ type: 'success', message: 'Transaction successful!', txid: signature }); 50 | } catch (error: any) { 51 | notify({ type: 'error', message: `Transaction failed!`, description: error?.message, txid: signature }); 52 | console.log('error', `Transaction failed! ${error?.message}`, signature); 53 | return; 54 | } 55 | }, [publicKey, notify, connection, sendTransaction]); 56 | 57 | return ( 58 |
59 |
60 |
62 | 73 |
74 |
75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /src/components/AppBar.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import dynamic from 'next/dynamic'; 3 | import React, { useState } from "react"; 4 | import { useAutoConnect } from '../contexts/AutoConnectProvider'; 5 | import NetworkSwitcher from './NetworkSwitcher'; 6 | import NavElement from './nav-element'; 7 | import { NFTDropdown } from './Dropdown/NFTDropdown'; 8 | import { TokenDropdown } from './Dropdown/TokenDropdown'; 9 | 10 | const WalletMultiButtonDynamic = dynamic( 11 | async () => (await import('@solana/wallet-adapter-react-ui')).WalletMultiButton, 12 | { ssr: false } 13 | ); 14 | 15 | export const AppBar: React.FC = () => { 16 | const { autoConnect, setAutoConnect } = useAutoConnect(); 17 | const [isNavOpen, setIsNavOpen] = useState(false); 18 | return ( 19 |
20 | {/* NavBar / Header */} 21 |
22 |
23 |
24 | 25 | Solana-Tools 26 | 27 |
28 |
29 | 30 | {/* Nav Links */} 31 | {/* Wallet & Settings */} 32 |
33 |
34 | setIsNavOpen(false)} 38 | /> 39 | 40 | 41 | setIsNavOpen(false)} 45 | /> 46 | 47 |
48 |