├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── components.json ├── next-env.d.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public └── og-image.jpg ├── src ├── app │ ├── favicon.ico │ ├── globals.css │ ├── inspector │ │ └── page.tsx │ ├── layout.tsx │ ├── not-found.tsx │ └── page.tsx ├── components │ ├── icons.tsx │ ├── inspector-action-form.tsx │ ├── inspector-form.tsx │ ├── inspector-table.tsx │ ├── json-code-block.tsx │ ├── main-nav.tsx │ ├── mobile-nav.tsx │ ├── site-footer.tsx │ ├── theme-mode-toggle.tsx │ ├── theme-provider.tsx │ └── ui │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── navigation-menu.tsx │ │ ├── table.tsx │ │ └── tabs.tsx ├── config │ └── site.ts ├── hooks │ ├── use-lock-body.ts │ └── use-mounted.ts ├── lib │ ├── actionsJsonMapper.ts │ ├── inspector.ts │ ├── structs.ts │ └── utils.ts └── types │ └── index.d.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | 4 | .DS_Store 5 | 6 | node_modules 7 | build 8 | .env 9 | .next 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": false, 5 | "bracketSpacing": true, 6 | "semi": true, 7 | "trailingComma": "all", 8 | "proseWrap": "always", 9 | "arrowParens": "always", 10 | "printWidth": 80 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 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). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | 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. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blinks-xyz", 3 | "author": "Solana Maintainers ", 4 | "repository": "https://github.com/solana-developers/blinks-xyz", 5 | "version": "0.1.0", 6 | "private": true, 7 | "scripts": { 8 | "dev": "next dev", 9 | "build": "next build", 10 | "start": "next start", 11 | "lint": "next lint" 12 | }, 13 | "dependencies": { 14 | "@hookform/resolvers": "^3.6.0", 15 | "@metaplex-foundation/mpl-token-metadata": "^3.2.1", 16 | "@metaplex-foundation/umi": "^0.9.2", 17 | "@metaplex-foundation/umi-bundle-defaults": "^0.9.2", 18 | "@metaplex-foundation/umi-uploader-irys": "^0.9.2", 19 | "@radix-ui/react-dialog": "^1.0.5", 20 | "@radix-ui/react-dropdown-menu": "^2.0.6", 21 | "@radix-ui/react-label": "^2.0.2", 22 | "@radix-ui/react-navigation-menu": "^1.1.4", 23 | "@radix-ui/react-slot": "^1.0.2", 24 | "@radix-ui/react-tabs": "^1.1.0", 25 | "@solana/actions": "^1.0.1", 26 | "@solana/web3.js": "^1.93.1", 27 | "class-variance-authority": "^0.7.0", 28 | "clsx": "^2.1.1", 29 | "lucide-react": "^0.390.0", 30 | "next": "14.2.3", 31 | "next-themes": "^0.3.0", 32 | "react": "^18", 33 | "react-dom": "^18", 34 | "react-hook-form": "^7.51.5", 35 | "tailwind-merge": "^2.3.0", 36 | "tailwindcss-animate": "^1.0.7", 37 | "vaul": "^0.9.1", 38 | "zod": "^3.23.8" 39 | }, 40 | "devDependencies": { 41 | "@types/node": "^20", 42 | "@types/react": "^18", 43 | "@types/react-dom": "^18", 44 | "eslint": "^8", 45 | "eslint-config-next": "14.2.3", 46 | "postcss": "^8", 47 | "tailwindcss": "^3.4.1", 48 | "typescript": "^5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/og-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/blinks-xyz/4134d6020a666c4236de1dfef023da3c320240b3/public/og-image.jpg -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-developers/blinks-xyz/4134d6020a666c4236de1dfef023da3c320240b3/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 224 71.4% 4.1%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 224 71.4% 4.1%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 224 71.4% 4.1%; 15 | 16 | --primary: 220.9 39.3% 11%; 17 | --primary-foreground: 210 20% 98%; 18 | 19 | --secondary: 220 14.3% 95.9%; 20 | --secondary-foreground: 220.9 39.3% 11%; 21 | 22 | --muted: 0 0% 90%; 23 | --muted-foreground: 220 8.9% 46.1%; 24 | 25 | --accent: 220 14.3% 95.9%; 26 | --accent-foreground: 220.9 39.3% 11%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 20% 98%; 30 | 31 | --success: 136 48% 49%; 32 | --success-foreground: 210 20% 98%; 33 | 34 | --warning: 38 77% 61%; 35 | --warning-foreground: 210 20% 98%; 36 | 37 | --border: 220 13% 91%; 38 | --input: 220 13% 91%; 39 | --ring: 224 71.4% 4.1%; 40 | 41 | --radius: 0.5rem; 42 | --solana-purple: #9945ff; 43 | --solana-green: #14f195; 44 | } 45 | 46 | .dark { 47 | --background: 224 71.4% 4.1%; 48 | --foreground: 210 20% 98%; 49 | 50 | --card: 224 71.4% 4.1%; 51 | --card-foreground: 210 20% 98%; 52 | 53 | --popover: 224 71.4% 4.1%; 54 | --popover-foreground: 210 20% 98%; 55 | 56 | --primary: 210 20% 98%; 57 | --primary-foreground: 220.9 39.3% 11%; 58 | 59 | --secondary: 215 27.9% 16.9%; 60 | --secondary-foreground: 210 20% 98%; 61 | 62 | --muted: 215 27.9% 16.9%; 63 | --muted-foreground: 217.9 10.6% 64.9%; 64 | 65 | --accent: 215 27.9% 16.9%; 66 | --accent-foreground: 210 20% 98%; 67 | 68 | --destructive: 0 62.8% 30.6%; 69 | --destructive-foreground: 210 20% 98%; 70 | 71 | --success: 136 54% 43%; 72 | --success-foreground: 210 20% 98%; 73 | 74 | --warning: 38 94% 40%; 75 | --warning-foreground: 210 20% 98%; 76 | 77 | --border: 215 27.9% 16.9%; 78 | --input: 215 27.9% 16.9%; 79 | --ring: 216 12.2% 83.9%; 80 | } 81 | } 82 | 83 | @layer base { 84 | * { 85 | @apply border-border; 86 | } 87 | body { 88 | @apply bg-background text-foreground; 89 | } 90 | 91 | .text-solana-gradient { 92 | @apply text-transparent bg-clip-text bg-gradient-to-r; 93 | @apply from-solana-purple to-solana-green; 94 | /* @apply hover:from-solana-green hover:to-solana-purple; */ 95 | } 96 | .text-solana-gradient-reverse { 97 | @apply text-solana-gradient bg-gradient-to-l; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/app/inspector/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | import { InspectorForm } from "@/components/inspector-form"; 3 | import { Suspense } from "react"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Inspector", 7 | alternates: { 8 | canonical: "/inspector", 9 | }, 10 | }; 11 | 12 | export default function Page() { 13 | return ( 14 | <> 15 |
16 |
17 | {/* 22 | Follow along on Twitter 23 | */} 24 | 25 |

26 | Blink Inspector 27 |

28 |

29 | Enter the URL of a Solana Action or Blink 30 |

31 |
32 |
33 | 34 | 35 | 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import Link from "next/link"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | import { Button, buttonVariants } from "@/components/ui/button"; 8 | import { MainNav } from "@/components/main-nav"; 9 | import { SiteFooter } from "@/components/site-footer"; 10 | import { ThemeProvider } from "@/components/theme-provider"; 11 | import { ThemeModeToggle } from "@/components/theme-mode-toggle"; 12 | import { navigationConfig, siteConfig } from "@/config/site"; 13 | 14 | const inter = Inter({ subsets: ["latin"] }); 15 | 16 | export const metadata: Metadata = { 17 | metadataBase: new URL(siteConfig.url), 18 | title: { 19 | template: `${siteConfig.name} - %s`, 20 | default: `${siteConfig.name} - Interface with users anywhere you can post a (b)link.`, 21 | }, 22 | openGraph: { 23 | images: "/og-image.jpg", 24 | }, 25 | twitter: { 26 | card: "summary_large_image", 27 | creator: siteConfig.twitterHandle, 28 | }, 29 | description: siteConfig.description, 30 | }; 31 | 32 | export default function RootLayout({ 33 | children, 34 | }: Readonly<{ 35 | children: React.ReactNode; 36 | }>) { 37 | return ( 38 | 39 | 40 | 41 |
42 |
43 |
44 | 45 | 46 | 62 |
63 |
64 |
{children}
65 | 66 |
67 |
68 | 69 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | import Link from "next/link"; 3 | 4 | import { siteConfig } from "@/config/site"; 5 | import { cn } from "@/lib/utils"; 6 | import { buttonVariants } from "@/components/ui/button"; 7 | 8 | export const metadata: Metadata = { 9 | title: "Page not found", 10 | }; 11 | 12 | export default function Page() { 13 | return ( 14 | <> 15 |
16 |
17 |

18 | Page Not Found 19 |

20 |

21 | We must have blinked too fast. 22 |
23 | We could not find the page you are looking for. 24 |

25 |
26 | 27 | Take me home 28 | 29 | 35 | Read the Docs 36 | 37 |
38 |
39 |
40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | import Link from "next/link"; 3 | 4 | import { siteConfig } from "@/config/site"; 5 | import { cn } from "@/lib/utils"; 6 | import { buttonVariants } from "@/components/ui/button"; 7 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 8 | import { 9 | BookOpenTextIcon, 10 | CodeIcon, 11 | HeartHandshakeIcon, 12 | LightbulbIcon, 13 | MessageCircleHeartIcon, 14 | SwatchBookIcon, 15 | } from "lucide-react"; 16 | 17 | export const metadata: Metadata = { 18 | alternates: { 19 | canonical: "/", 20 | }, 21 | }; 22 | 23 | // 24 | const resourceCards: Array<{ 25 | title: string; 26 | href: string; 27 | description: React.ReactNode; 28 | icon: React.ReactNode; 29 | }> = [ 30 | { 31 | href: siteConfig.links.docs, 32 | title: "Documentation", 33 | description: 34 | "Read the documentation and spec for Solana Actions and blinks.", 35 | icon: , 36 | }, 37 | { 38 | href: "https://www.npmjs.com/package/@solana/actions", 39 | title: "@solana/actions SDK", 40 | description: "Quickly get started building actions using the NPM package.", 41 | icon: , 42 | }, 43 | { 44 | href: "https://github.com/solana-developers/solana-actions/tree/main/examples", 45 | title: "Code Samples", 46 | description: "See example code for how to build Actions into your project.", 47 | icon: , 48 | }, 49 | { 50 | href: "https://dial.to", 51 | title: "Dialect's Dial.to", 52 | description: "Use dial.to in order to test your blinks with a wallet.", 53 | icon: , 54 | }, 55 | { 56 | href: "https://github.com/solana-developers/awesome-blinks", 57 | title: "Awesome Blinks", 58 | description: 59 | "Collection of awesome resources for blinks and live ones in the wild.", 60 | icon: , 61 | }, 62 | { 63 | href: "https://github.com/solana-developers/awesome-blinks/discussions/categories/ideas-for-blinks", 64 | title: "Ideas for Blinks", 65 | description: "Get inspired and see what new ideas people have for blinks.", 66 | icon: , 67 | }, 68 | ]; 69 | 70 | export default function Page() { 71 | return ( 72 | <> 73 |
74 |
75 | {/* 80 | Follow along on Twitter 81 | */} 82 | 83 |

84 | Bring Crypto to the People with{" "} 85 | 86 | Solana Actions 87 | 88 | {" and "} 89 | 90 | Blockchain Links 91 | 92 |

93 |

94 | Interface with users anywhere you can post a (b)link. 95 |

96 |
97 | 102 | Learn More 103 | 104 | 110 | Read the Docs 111 | 112 |
113 |
114 | 118 | Blink Inspector 119 | 120 |
121 |
122 |
123 |
127 |
128 |

129 | Resources 130 |

131 |

132 | Discover the growing ecosystem of tooling and resources for Solana 133 | Actions and blinks. 134 |

135 |
136 | 137 |
138 | {resourceCards.map((item, key) => ( 139 | 145 | 146 | 147 | 148 | {item.icon} 149 | 150 | {item.title} 151 | 152 | 153 | 154 | 155 |

{item.description}

156 |
157 |
158 | 159 | ))} 160 |
161 | {/*
162 |

163 |
*/} 164 |
165 | 166 | ); 167 | } 168 | -------------------------------------------------------------------------------- /src/components/icons.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AlertTriangle, 3 | ArrowRight, 4 | Check, 5 | ChevronLeft, 6 | ChevronRight, 7 | Command, 8 | CreditCard, 9 | File, 10 | FileText, 11 | HelpCircle, 12 | Image, 13 | Laptop, 14 | Loader2, 15 | LucideProps, 16 | MenuIcon, 17 | Moon, 18 | MoreVertical, 19 | Pizza, 20 | Plus, 21 | Settings, 22 | SunMedium, 23 | Trash, 24 | Twitter, 25 | User, 26 | X, 27 | type Icon as LucideIcon, 28 | } from "lucide-react"; 29 | 30 | export type Icon = typeof LucideIcon; 31 | 32 | export const Icons = { 33 | logo: Command, 34 | menu: MenuIcon, 35 | close: X, 36 | spinner: Loader2, 37 | chevronLeft: ChevronLeft, 38 | chevronRight: ChevronRight, 39 | trash: Trash, 40 | post: FileText, 41 | page: File, 42 | media: Image, 43 | settings: Settings, 44 | billing: CreditCard, 45 | ellipsis: MoreVertical, 46 | add: Plus, 47 | warning: AlertTriangle, 48 | user: User, 49 | arrowRight: ArrowRight, 50 | help: HelpCircle, 51 | pizza: Pizza, 52 | sun: SunMedium, 53 | moon: Moon, 54 | laptop: Laptop, 55 | gitHub: ({ ...props }: LucideProps) => ( 56 | 71 | ), 72 | twitter: Twitter, 73 | check: Check, 74 | }; 75 | -------------------------------------------------------------------------------- /src/components/inspector-action-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | Form, 6 | FormControl, 7 | FormDescription, 8 | FormField, 9 | FormItem, 10 | FormLabel, 11 | FormMessage, 12 | } from "@/components/ui/form"; 13 | import { Input } from "@/components/ui/input"; 14 | import { ActionPostResponse, LinkedAction, parseURL } from "@solana/actions"; 15 | import { zodResolver } from "@hookform/resolvers/zod"; 16 | import { useForm } from "react-hook-form"; 17 | import { z } from "zod"; 18 | import { 19 | Card, 20 | CardContent, 21 | CardDescription, 22 | CardFooter, 23 | CardHeader, 24 | CardTitle, 25 | } from "@/components/ui/card"; 26 | import { InspectorRow, InspectorRowStatus } from "./inspector-table"; 27 | import { 28 | Table, 29 | TableBody, 30 | TableCaption, 31 | TableCell, 32 | TableHead, 33 | TableHeader, 34 | TableRow, 35 | } from "@/components/ui/table"; 36 | import { 37 | ActionCORSError, 38 | InspectorPayload, 39 | InspectorRequest, 40 | inspectPost, 41 | } from "@/lib/inspector"; 42 | import { Fragment, useMemo, useState } from "react"; 43 | import { useSearchParams } from "next/navigation"; 44 | import { Keypair } from "@solana/web3.js"; 45 | import { 46 | ChevronDownIcon, 47 | ChevronUpIcon, 48 | CornerDownRightIcon, 49 | } from "lucide-react"; 50 | import { JsonCodeBlock } from "./json-code-block"; 51 | 52 | export function InspectorActionForm({ 53 | id, 54 | linkedAction, 55 | apiEndpoint, 56 | getEndpointUrl, 57 | }: { 58 | linkedAction: LinkedAction; 59 | apiEndpoint: string; 60 | getEndpointUrl: URL; 61 | id: number; 62 | }) { 63 | const DEFAULT_RESPONSE_OBJECT: InspectorRequest = { 64 | checked: false, 65 | status: 0, 66 | url: getEndpointUrl, 67 | corsAccessible: false, 68 | structured: false, 69 | data: undefined, 70 | headers: undefined, 71 | }; 72 | 73 | const params = useSearchParams(); 74 | 75 | const [showMoreData, setShowMoreData] = useState(true); 76 | const [showMoreHeaders, setShowMoreHeaders] = useState(false); 77 | 78 | // const [hasCorsError, setHasCorsError] = useState(false); 79 | const [loading, setLoading] = useState(false); 80 | 81 | const [postResponse, setPostResponse] = useState< 82 | NonNullable 83 | >(DEFAULT_RESPONSE_OBJECT); 84 | 85 | const url: URL | false = useMemo( 86 | () => 87 | !!params.get("url") 88 | ? new URL(decodeURIComponent(params.get("url")!)) 89 | : false, 90 | [params], 91 | ); 92 | 93 | const formSchema = z.object({ 94 | account: z.string(), 95 | username: z.string().min(2, { 96 | message: "Username must be at least 2 characters.", 97 | }), 98 | }); 99 | 100 | type FormInput = z.infer; 101 | 102 | // 1. Define your form. 103 | const form = useForm({ 104 | // resolver: zodResolver(formSchema), 105 | defaultValues: { 106 | account: Keypair.generate().publicKey.toBase58(), 107 | }, 108 | }); 109 | 110 | // 2. Define a submit handler. 111 | // async function onSubmit(values: FormInput) { 112 | async function onSubmit(e: React.FormEvent) { 113 | e.preventDefault(); 114 | if (loading) return; 115 | 116 | setLoading(true); 117 | // setPostResponse(DEFAULT_RESPONSE_OBJECT); 118 | 119 | if (!url) { 120 | alert("no 'url' was found"); 121 | return; 122 | } 123 | 124 | const values = form.getValues(); 125 | 126 | // compute the final api url based on the user input 127 | let apiUrl = apiEndpoint; 128 | 129 | linkedAction.parameters?.map((param) => { 130 | // replace any string literals 131 | const template = "{" + param.name + "}"; 132 | // @ts-ignore 133 | const value: string = values[param.name] || template; 134 | 135 | // replace template literals 136 | if (apiUrl.includes(template) || apiUrl.includes(encodeURI(template))) { 137 | apiUrl = apiUrl.replaceAll(template, value); 138 | apiUrl = apiUrl.replaceAll(encodeURI(template), value); 139 | } 140 | }); 141 | 142 | console.log("apiUrl:", apiUrl); 143 | 144 | try { 145 | const res = await inspectPost(apiUrl, { 146 | account: values.account, 147 | }); 148 | 149 | console.log("got this response"); 150 | console.log(res); 151 | 152 | setPostResponse(res); 153 | } catch (err) { 154 | if (err instanceof ActionCORSError) { 155 | // setHasCorsError(true); 156 | } 157 | console.error("[inspectPost]", "[unknown error]", err); 158 | } 159 | 160 | if (!postResponse.structured) { 161 | setShowMoreData(true); 162 | // setShowMoreHeaders(true); 163 | } 164 | 165 | setLoading(false); 166 | } 167 | 168 | return ( 169 | 170 |
171 | onSubmit(e)} 173 | // className="space-y-8" 174 | aria-disabled={loading} 175 | > 176 | 177 |
178 | Action #{id + 1} 179 | {/* */} 186 |
187 |
188 | 189 | 190 | 194 | {/* 195 | 196 | Field 197 | Value 198 | 199 | */} 200 | 201 | 202 | label 203 | {linkedAction.label} 204 | 205 | 206 | href 207 | {linkedAction.href} 208 | 209 | 210 | 211 | mapped to 212 | 213 | 214 | {apiEndpoint || "[err] no mapping url found"} 215 | {/* {decodeURI(postResponse.url.toString()) || 216 | "[err] no mapping url found"} */} 217 | 218 | {/* {!!postResponse?.mappedUrl 219 | ? decodeURI(postResponse.mappedUrl.toString()) 220 | : "[err] no mapping url found"} */} 221 | 222 | 223 | 224 | {linkedAction.parameters ? ( 225 | 226 | 227 |

228 | parameters: {linkedAction.parameters.length} 229 |

230 | 231 |
232 | 233 | 234 | Index 235 | Field 236 | Value 237 | 238 | 239 | 240 | 241 | {linkedAction.parameters.map((param, key) => ( 242 | 243 | 244 | 248 | #{key} 249 | 250 | 251 | name 252 | 253 | 254 | {param.name} 255 | 256 | 257 | 258 | 259 | label 260 | 261 | 262 | {param.label || "no 'label' set"} 263 | 264 | 265 | 266 | ))} 267 | 268 |
269 | 270 | 271 | ) : ( 272 | 273 | parameters 274 | none detected 275 | 276 | )} 277 | 278 | 279 |
280 | 281 | 282 | Test this Action 283 | 284 | ( 290 | 291 | Account Address 292 | 293 | 299 | 300 | 301 | {/* Required: {param.required || false} */} 302 | {/* This is your public display name. */} 303 | 304 | 305 | 306 | )} 307 | /> 308 | 309 | {linkedAction.parameters?.map((param, key) => ( 310 | ( 318 | 319 | 320 | {param.name || "[error: no 'name' field]"} 321 | 322 | 323 | 330 | 331 | {/* 332 | Required: {param.required || false} 333 | This is your public display name. 334 | */} 335 | 336 | 337 | )} 338 | /> 339 | ))} 340 | 341 | 344 | 345 | 346 | {postResponse.checked && ( 347 | 348 | 349 | {/* 350 | Read the{" "} 351 | 355 | Solana Actions 356 | {" "} 357 | documentation for more details 358 | */} 359 | 360 | 361 | Status 362 | Name 363 | 364 | 365 | 366 | 367 | 371 | 377 | 381 | 382 | {!!postResponse?.corsAccessible && ( 383 | <> 384 | 385 | 386 | 390 | 391 | 392 |
393 |
Response data
394 | 419 |
420 | 421 | 426 |
427 |
428 | 429 | 430 | 431 | 435 | 436 | 437 |
438 |
Response headers
439 | 466 |
467 | 468 | 473 |
474 |
475 | 476 | )} 477 |
478 |
479 |
480 | )} 481 |
482 | 483 |
484 | ); 485 | } 486 | -------------------------------------------------------------------------------- /src/components/inspector-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Table, 5 | TableBody, 6 | TableHead, 7 | TableHeader, 8 | TableRow, 9 | } from "@/components/ui/table"; 10 | import { 11 | ActionCORSError, 12 | inspectActionsJson, 13 | inspectGet, 14 | InspectorPayload, 15 | linkedActionHref, 16 | } from "@/lib/inspector"; 17 | import { useRouter, useSearchParams } from "next/navigation"; 18 | import { useEffect, useMemo, useState } from "react"; 19 | import { Button } from "./ui/button"; 20 | import { Input } from "./ui/input"; 21 | 22 | import { InspectorActionForm } from "./inspector-action-form"; 23 | import { InspectorRow } from "./inspector-table"; 24 | import { ActionsURLMapper } from "@/lib/actionsJsonMapper"; 25 | import { ActionsJson, BlinkURLFields, parseURL } from "@solana/actions"; 26 | 27 | export function InspectorForm({ className }: { className?: string }) { 28 | const params = useSearchParams(); 29 | const router = useRouter(); 30 | const [inputText, setInputText] = useState(params.get("url") || ""); 31 | const [hasCorsError, setHasCorsError] = useState(false); 32 | const [loading, setLoading] = useState(false); 33 | const [inspector, setInspector] = useState(null); 34 | 35 | const submitHandler = (e: React.FormEvent) => { 36 | e.preventDefault(); 37 | 38 | let isBlink = false; 39 | 40 | try { 41 | let parsedUrl = parseURL(inputText); 42 | console.log("parsedUrl input:", parsedUrl); 43 | 44 | if (!!(parsedUrl as BlinkURLFields)?.blink) { 45 | isBlink = true; 46 | router.push( 47 | `/inspector?url=${encodeURIComponent( 48 | (parsedUrl as BlinkURLFields).action.link.toString(), 49 | )}`, 50 | {}, 51 | ); 52 | } 53 | } catch (err) {} 54 | 55 | if (!isBlink) { 56 | router.push(`/inspector?url=${encodeURIComponent(inputText)}`, {}); 57 | } 58 | }; 59 | 60 | const url: URL | false = useMemo( 61 | () => 62 | !!params.get("url") 63 | ? new URL(decodeURIComponent(params.get("url")!)) 64 | : false, 65 | [params], 66 | ); 67 | 68 | async function inspectBlinkUrl(url: URL) { 69 | if (loading) return; 70 | 71 | setLoading(true); 72 | setInspector(null); 73 | 74 | const payload: InspectorPayload = { 75 | actionsJson: undefined, 76 | getResponse: undefined, 77 | // postResponse: undefined, 78 | mappedActionsJsonUrl: new URL("actions.json", url.origin), 79 | mappedGetUrl: url, 80 | }; 81 | 82 | try { 83 | // todo: if not http or https 84 | // todo: should this also support solana: and solana-action: ?? 85 | 86 | payload.actionsJson = await inspectActionsJson( 87 | payload.mappedActionsJsonUrl, 88 | ); 89 | } catch (err) { 90 | if (err instanceof ActionCORSError) { 91 | setHasCorsError(true); 92 | } 93 | console.error("[inspectActionsJson]", "[unknown error]", err); 94 | } 95 | 96 | // map the GET endpoint via actions.json 97 | if ( 98 | payload.actionsJson?.data && 99 | typeof payload.actionsJson.data != "string" 100 | ) { 101 | const actionsUrlMapper = new ActionsURLMapper( 102 | payload.actionsJson.data as ActionsJson, 103 | ); 104 | 105 | const mappedUrl = actionsUrlMapper.mapUrl(url); 106 | if (mappedUrl) { 107 | payload.mappedGetUrl = mappedUrl; 108 | } 109 | } 110 | 111 | try { 112 | payload.getResponse = await inspectGet(payload.mappedGetUrl); 113 | } catch (err) { 114 | if (err instanceof ActionCORSError) { 115 | setHasCorsError(true); 116 | } 117 | console.error("[inspectGet]", "[unknown error]", err); 118 | } 119 | 120 | // try { 121 | // payload.postResponse = await inspectPost(url, { 122 | // account: "7igbU6EsyjtqryUispbReSnbkZVXTDskQmsFPyh38u6E", 123 | // }); 124 | // } catch (err) { 125 | // if (err instanceof ActionCORSError) { 126 | // setHasCorsError(true); 127 | // } 128 | // console.error("[inspectPost]", "[unknown error]", err); 129 | // } 130 | 131 | setInspector(payload); 132 | setLoading(false); 133 | } 134 | 135 | useEffect(() => { 136 | if (!url) return console.log("no url found yet"); 137 | inspectBlinkUrl(url); 138 | }, [url]); 139 | 140 | return ( 141 |
142 |
147 | { 153 | setInputText(e.target.value); 154 | }} 155 | placeholder="Enter a URL..." 156 | className="min-w-96" 157 | disabled={loading} 158 | aria-disabled={loading} 159 | /> 160 | 163 |
164 | 165 | {/* 166 | 167 | Overview 168 | Actions (3) 169 | 170 | 171 | Make changes to your account here. 172 | 173 | Change your password here. 174 | */} 175 | 176 |
177 | {inspector && ( 178 | <> 179 | {/* */} 184 | 185 | 189 | 190 | 191 | Status 192 | Name 193 | 194 | Status 195 | 196 | 197 | 198 | 199 | 200 | 203 |

actions.json file

204 |

205 | {inspector.mappedActionsJsonUrl.toString() || 206 | "[err] no actions.json url found"} 207 |

208 | 209 | } 210 | corsAccessible={inspector.actionsJson?.corsAccessible} 211 | status={ 212 | inspector.actionsJson?.corsAccessible && 213 | inspector.actionsJson?.status == 200 && 214 | inspector.actionsJson?.structured 215 | ? "valid" 216 | : "error" 217 | } 218 | responseData={inspector.actionsJson?.data} 219 | responseHeaders={inspector.actionsJson?.headers} 220 | childItems={ 221 | !inspector.actionsJson?.corsAccessible 222 | ? [ 223 | { 224 | name: "Cross-Origin Resource Headers", 225 | status: inspector.actionsJson?.corsAccessible 226 | ? "valid" 227 | : "error", 228 | }, 229 | ] 230 | : [ 231 | { 232 | name: "Cross-Origin Resource Headers", 233 | status: inspector.actionsJson?.corsAccessible 234 | ? "valid" 235 | : "error", 236 | }, 237 | { 238 | name: `HTTP status code of ${ 239 | inspector.actionsJson?.status || "[err]" 240 | }`, 241 | status: 242 | inspector.actionsJson?.status == 200 243 | ? "valid" 244 | : "error", 245 | }, 246 | { 247 | name: "Validate structure of the returned data", 248 | status: inspector.actionsJson?.structured 249 | ? "valid" 250 | : "error", 251 | }, 252 | ] 253 | } 254 | >
255 | 256 | 259 |

GET endpoint and response

260 |

261 | {inspector?.mappedGetUrl?.toString() || 262 | inspector.getResponse?.url.toString() || 263 | "[err] no GET endpoint url found"} 264 |

265 | 266 | } 267 | corsAccessible={inspector.getResponse?.corsAccessible} 268 | status={ 269 | inspector.getResponse?.corsAccessible && 270 | inspector.getResponse?.status == 200 && 271 | inspector.getResponse?.structured 272 | ? "valid" 273 | : "error" 274 | } 275 | responseData={inspector.getResponse?.data} 276 | responseHeaders={inspector.getResponse?.headers} 277 | childItems={ 278 | !inspector.getResponse?.corsAccessible 279 | ? [ 280 | { 281 | name: "Cross-Origin Resource Headers", 282 | status: inspector.getResponse?.corsAccessible 283 | ? "valid" 284 | : "error", 285 | }, 286 | ] 287 | : [ 288 | { 289 | name: !!inspector.mappedGetUrl 290 | ? "Mapped via actions.json to create blinks" 291 | : "NOT mapped via actions.json - No blink will be displayed", 292 | status: inspector.mappedGetUrl 293 | ? "valid" 294 | : "warning", 295 | }, 296 | { 297 | name: "Cross-Origin Resource Headers", 298 | status: inspector.getResponse?.corsAccessible 299 | ? "valid" 300 | : "error", 301 | }, 302 | { 303 | name: `HTTP status code of ${ 304 | inspector.getResponse?.status || "[err]" 305 | }`, 306 | status: 307 | inspector.getResponse?.status == 200 308 | ? "valid" 309 | : "error", 310 | }, 311 | { 312 | name: "Validate structure of the returned data", 313 | status: inspector.getResponse?.structured 314 | ? "valid" 315 | : "error", 316 | }, 317 | ] 318 | } 319 | >
320 |
321 |
322 | 323 | {!!inspector?.getResponse?.data && 324 | inspector.getResponse.url && 325 | typeof inspector.getResponse.data != "string" && ( 326 |
327 | {/* 328 | 329 | Read the{" "} 330 | 334 | Solana Actions 335 | {" "} 336 | documentation for more details 337 | 338 | 339 | 340 | Field 341 | Value 342 | 343 | 344 | 345 | 346 | icon 347 | 348 | {inspector.getResponse.data.icon} 349 | 350 | 351 | 352 | title 353 | 354 | {inspector.getResponse.data.title} 355 | 356 | 357 | 358 | 359 | description 360 | 361 | 362 | {inspector.getResponse.data.description} 363 | 364 | 365 | 366 |
*/} 367 | 368 | {inspector.getResponse.data.links?.actions ? ( 369 | inspector.getResponse.data.links?.actions.map( 370 | (linkedAction, key) => { 371 | return ( 372 | 382 | ); 383 | }, 384 | ) 385 | ) : ( 386 | 400 | )} 401 | 402 | {/*
403 |                     {JSON.stringify(inspector.getResponse.data, null, "\t")}
404 |                   
*/} 405 |
406 | )} 407 | 408 | )} 409 |
410 |
411 | ); 412 | } 413 | -------------------------------------------------------------------------------- /src/components/inspector-table.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Table, 5 | TableBody, 6 | TableCaption, 7 | TableCell, 8 | TableHead, 9 | TableHeader, 10 | TableRow, 11 | } from "@/components/ui/table"; 12 | import { 13 | ActionCORSError, 14 | inspectActionsJson, 15 | inspectGet, 16 | InspectorPayload, 17 | inspectPost, 18 | } from "@/lib/inspector"; 19 | import { useState } from "react"; 20 | import { Badge } from "./ui/badge"; 21 | import { Button } from "./ui/button"; 22 | import { 23 | ChevronDownIcon, 24 | ChevronUpIcon, 25 | CornerDownRightIcon, 26 | } from "lucide-react"; 27 | import { cn } from "@/lib/utils"; 28 | import { JsonCodeBlock } from "./json-code-block"; 29 | 30 | export function InspectorRow({ 31 | status, 32 | name, 33 | children, 34 | childItems, 35 | responseData, 36 | responseHeaders, 37 | corsAccessible, 38 | }: { 39 | name: React.ReactNode; 40 | children?: React.ReactNode; 41 | status: InspectorRowStatusProps["status"]; 42 | childItems?: Array; 43 | responseData?: any; 44 | responseHeaders?: any; 45 | corsAccessible?: boolean; 46 | }) { 47 | const [showMoreData, setShowMoreData] = useState(false); 48 | const [showMoreHeaders, setShowMoreHeaders] = useState(false); 49 | 50 | return ( 51 | <> 52 | 53 | 54 | 55 | 56 | 57 | {name} 58 | 59 | {/* 60 | 61 | */} 62 | 63 | 64 | {childItems?.map((item, key) => ( 65 | 66 | ))} 67 | 68 | {!!corsAccessible && ( 69 | <> 70 | 71 | 72 | 73 | 74 | 75 |
76 |
Response data
77 | 96 |
97 | 98 | 103 |
104 |
105 | 106 | 107 | 108 | 109 | 110 | 111 |
112 |
Response headers
113 | 132 |
133 | 134 | 139 |
140 |
141 | 142 | )} 143 | 144 | ); 145 | } 146 | 147 | export type InspectorRowChildItem = { 148 | name: string; 149 | // children?: React.ReactNode; 150 | status: InspectorRowStatusProps["status"]; 151 | }; 152 | 153 | export function InspectorRowChild({ status, name }: InspectorRowChildItem) { 154 | return ( 155 | 156 | 157 | 158 | 159 | {name} 160 | 161 | 162 | 163 | 164 | 165 | ); 166 | } 167 | 168 | export type InspectorRowStatusProps = { 169 | status: "error" | "valid" | "warning" | "not-tested"; 170 | label?: string; 171 | }; 172 | 173 | export function InspectorRowStatus({ status, label }: InspectorRowStatusProps) { 174 | switch (status) { 175 | case "valid": 176 | return {label || "Valid"}; 177 | case "error": 178 | return {label || "Error"}; 179 | case "warning": 180 | return {label || "Warning"}; 181 | case "not-tested": 182 | return {label || "Not Tested"}; 183 | default: 184 | return {label || "Unknown"}; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/components/json-code-block.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | export function JsonCodeBlock({ 4 | data, 5 | showMore, 6 | errorMessage, 7 | }: { 8 | data?: object | string; 9 | showMore: boolean; 10 | errorMessage: string; 11 | }) { 12 | if (!!data && typeof data == "string") { 13 | try { 14 | data = JSON.parse(data); 15 | } catch (err) { 16 | console.log("[error] unable to parse 'data' as json"); 17 | } 18 | } 19 | 20 | return ( 21 |
27 |       {data ? JSON.stringify(data, null, "\t") : errorMessage}
28 |     
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/main-nav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import Link from "next/link"; 5 | import { useSelectedLayoutSegment } from "next/navigation"; 6 | 7 | import { MainNavItem } from "@/types"; 8 | import { siteConfig } from "@/config/site"; 9 | import { cn } from "@/lib/utils"; 10 | import { Icons } from "@/components/icons"; 11 | import { MobileNav } from "@/components/mobile-nav"; 12 | import { Button } from "@/components/ui/button"; 13 | import { EyeIcon } from "lucide-react"; 14 | 15 | interface MainNavProps { 16 | items?: MainNavItem[]; 17 | children?: React.ReactNode; 18 | } 19 | 20 | export function MainNav({ items, children }: MainNavProps) { 21 | const segment = useSelectedLayoutSegment(); 22 | const [showMobileMenu, setShowMobileMenu] = React.useState(false); 23 | 24 | return ( 25 |
26 | 30 | {/* */} 31 | 32 | {siteConfig.name} 33 | 34 | 35 | {items?.length ? ( 36 | 54 | ) : null} 55 | 56 | {/* 63 | {showMobileMenu && items && ( 64 | {children} 65 | )} */} 66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/components/mobile-nav.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Link from "next/link"; 3 | 4 | import { MainNavItem } from "@/types"; 5 | import { siteConfig } from "@/config/site"; 6 | import { cn } from "@/lib/utils"; 7 | import { useLockBody } from "@/hooks/use-lock-body"; 8 | import { Icons } from "@/components/icons"; 9 | import { EyeIcon } from "lucide-react"; 10 | 11 | interface MobileNavProps { 12 | items: MainNavItem[]; 13 | children?: React.ReactNode; 14 | } 15 | 16 | export function MobileNav({ items, children }: MobileNavProps) { 17 | useLockBody(); 18 | 19 | return ( 20 |
25 |
26 | 27 | {/* */} 28 | 29 | {siteConfig.name} 30 | 31 | 45 | {children} 46 |
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/components/site-footer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { siteConfig } from "@/config/site"; 4 | import { cn } from "@/lib/utils"; 5 | import { ThemeModeToggle } from "@/components/theme-mode-toggle"; 6 | import { Button, buttonVariants } from "@/components/ui/button"; 7 | import Link from "next/link"; 8 | import { EyeIcon } from "lucide-react"; 9 | 10 | export function SiteFooter({ className }: React.HTMLAttributes) { 11 | return ( 12 |
13 |
14 |
15 | 16 |

17 | Maintained by{" "} 18 | 24 | @solana_devs 25 | 26 | {/* . Source code is available on{" "} 27 | 33 | GitHub 34 | 35 | . */} 36 |

37 |
38 | 54 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/components/theme-mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { useTheme } from "next-themes"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | import { 8 | DropdownMenu, 9 | DropdownMenuContent, 10 | DropdownMenuItem, 11 | DropdownMenuTrigger, 12 | } from "@/components/ui/dropdown-menu"; 13 | import { Icons } from "@/components/icons"; 14 | 15 | export function ThemeModeToggle() { 16 | const { setTheme } = useTheme(); 17 | 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | setTheme("light")}> 29 | 30 | Light 31 | 32 | setTheme("dark")}> 33 | 34 | Dark 35 | 36 | setTheme("system")}> 37 | 38 | System 39 | 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import { ThemeProviderProps } from "next-themes/dist/types"; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const badgeVariants = cva( 7 | "pointer-events-none inline-flex items-center rounded-full border px-2.5 py-0.5 text-sm font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | success: 18 | "border-transparent bg-success text-success-foreground hover:bg-success/80", 19 | warning: 20 | "border-transparent bg-warning text-warning-foreground hover:bg-warning/80", 21 | outline: "text-foreground", 22 | }, 23 | }, 24 | defaultVariants: { 25 | variant: "default", 26 | }, 27 | }, 28 | ); 29 | 30 | export interface BadgeProps 31 | extends React.HTMLAttributes, 32 | VariantProps {} 33 | 34 | function Badge({ className, variant, ...props }: BadgeProps) { 35 | return ( 36 |
37 | ); 38 | } 39 | 40 | export { Badge, badgeVariants }; 41 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline !text-base", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | }, 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button"; 45 | return ( 46 | 51 | ); 52 | }, 53 | ); 54 | Button.displayName = "Button"; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /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 | }; 87 | -------------------------------------------------------------------------------- /src/components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { Drawer as DrawerPrimitive } from "vaul" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Drawer = ({ 9 | shouldScaleBackground = true, 10 | ...props 11 | }: React.ComponentProps) => ( 12 | 16 | ) 17 | Drawer.displayName = "Drawer" 18 | 19 | const DrawerTrigger = DrawerPrimitive.Trigger 20 | 21 | const DrawerPortal = DrawerPrimitive.Portal 22 | 23 | const DrawerClose = DrawerPrimitive.Close 24 | 25 | const DrawerOverlay = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 34 | )) 35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName 36 | 37 | const DrawerContent = React.forwardRef< 38 | React.ElementRef, 39 | React.ComponentPropsWithoutRef 40 | >(({ className, children, ...props }, ref) => ( 41 | 42 | 43 | 51 |
52 | {children} 53 | 54 | 55 | )) 56 | DrawerContent.displayName = "DrawerContent" 57 | 58 | const DrawerHeader = ({ 59 | className, 60 | ...props 61 | }: React.HTMLAttributes) => ( 62 |
66 | ) 67 | DrawerHeader.displayName = "DrawerHeader" 68 | 69 | const DrawerFooter = ({ 70 | className, 71 | ...props 72 | }: React.HTMLAttributes) => ( 73 |
77 | ) 78 | DrawerFooter.displayName = "DrawerFooter" 79 | 80 | const DrawerTitle = React.forwardRef< 81 | React.ElementRef, 82 | React.ComponentPropsWithoutRef 83 | >(({ className, ...props }, ref) => ( 84 | 92 | )) 93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName 94 | 95 | const DrawerDescription = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, ...props }, ref) => ( 99 | 104 | )) 105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName 106 | 107 | export { 108 | Drawer, 109 | DrawerPortal, 110 | DrawerOverlay, 111 | DrawerTrigger, 112 | DrawerClose, 113 | DrawerContent, 114 | DrawerHeader, 115 | DrawerFooter, 116 | DrawerTitle, 117 | DrawerDescription, 118 | } 119 | -------------------------------------------------------------------------------- /src/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; 5 | import { Check, ChevronRight, Circle } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root; 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group; 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal; 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub; 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean; 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )); 40 | DropdownMenuSubTrigger.displayName = 41 | DropdownMenuPrimitive.SubTrigger.displayName; 42 | 43 | const DropdownMenuSubContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )); 56 | DropdownMenuSubContent.displayName = 57 | DropdownMenuPrimitive.SubContent.displayName; 58 | 59 | const DropdownMenuContent = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, sideOffset = 4, ...props }, ref) => ( 63 | 64 | 73 | 74 | )); 75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; 76 | 77 | const DropdownMenuItem = React.forwardRef< 78 | React.ElementRef, 79 | React.ComponentPropsWithoutRef & { 80 | inset?: boolean; 81 | } 82 | >(({ className, inset, ...props }, ref) => ( 83 | 92 | )); 93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; 94 | 95 | const DropdownMenuCheckboxItem = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, children, checked, ...props }, ref) => ( 99 | 108 | 109 | 110 | 111 | 112 | 113 | {children} 114 | 115 | )); 116 | DropdownMenuCheckboxItem.displayName = 117 | DropdownMenuPrimitive.CheckboxItem.displayName; 118 | 119 | const DropdownMenuRadioItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )); 139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; 140 | 141 | const DropdownMenuLabel = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef & { 144 | inset?: boolean; 145 | } 146 | >(({ className, inset, ...props }, ref) => ( 147 | 156 | )); 157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; 158 | 159 | const DropdownMenuSeparator = React.forwardRef< 160 | React.ElementRef, 161 | React.ComponentPropsWithoutRef 162 | >(({ className, ...props }, ref) => ( 163 | 168 | )); 169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; 170 | 171 | const DropdownMenuShortcut = ({ 172 | className, 173 | ...props 174 | }: React.HTMLAttributes) => { 175 | return ( 176 | 180 | ); 181 | }; 182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; 183 | 184 | export { 185 | DropdownMenu, 186 | DropdownMenuTrigger, 187 | DropdownMenuContent, 188 | DropdownMenuItem, 189 | DropdownMenuCheckboxItem, 190 | DropdownMenuRadioItem, 191 | DropdownMenuLabel, 192 | DropdownMenuSeparator, 193 | DropdownMenuShortcut, 194 | DropdownMenuGroup, 195 | DropdownMenuPortal, 196 | DropdownMenuSub, 197 | DropdownMenuSubContent, 198 | DropdownMenuSubTrigger, 199 | DropdownMenuRadioGroup, 200 | }; 201 | -------------------------------------------------------------------------------- /src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | import { Slot } from "@radix-ui/react-slot"; 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form"; 12 | 13 | import { cn } from "@/lib/utils"; 14 | import { Label } from "@/components/ui/label"; 15 | 16 | const Form = FormProvider; 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath, 21 | > = { 22 | name: TName; 23 | }; 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue, 27 | ); 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath, 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext); 44 | const itemContext = React.useContext(FormItemContext); 45 | const { getFieldState, formState } = useFormContext(); 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState); 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within "); 51 | } 52 | 53 | const { id } = itemContext; 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | }; 63 | }; 64 | 65 | type FormItemContextValue = { 66 | id: string; 67 | }; 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue, 71 | ); 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId(); 78 | 79 | return ( 80 | 81 |
82 | 83 | ); 84 | }); 85 | FormItem.displayName = "FormItem"; 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField(); 92 | 93 | return ( 94 |