├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── app ├── (app) │ ├── config │ │ └── page.tsx │ ├── create │ │ └── page.tsx │ ├── layout.tsx │ ├── page.tsx │ ├── settings │ │ └── page.tsx │ └── transactions │ │ └── page.tsx ├── favicon.ico ├── globals.css └── layout.tsx ├── components.json ├── components ├── AddMemberInput.tsx ├── ApproveButton.tsx ├── ChangeThresholdInput.tsx ├── ChangeUpgradeAuthorityInput.tsx ├── ConnectWalletButton.tsx ├── CreateSquadForm.tsx ├── CreateTransactionButton.tsx ├── ExecuteButton.tsx ├── MultisigInput.tsx ├── RejectButton.tsx ├── RemoveMemberButton.tsx ├── RenderMultisigRoute.tsx ├── SendSolButton.tsx ├── SendTokensButton.tsx ├── SetProgramIdInput.tsx ├── SetRpcUrlnput.tsx ├── TokenList.tsx ├── TransactionTable.tsx ├── VaultDisplayer.tsx ├── VaultSelector.tsx ├── Wallet.tsx └── ui │ ├── button.tsx │ ├── card.tsx │ ├── command.tsx │ ├── dialog.tsx │ ├── input.tsx │ ├── pagination.tsx │ ├── popover.tsx │ ├── select.tsx │ ├── sonner.tsx │ └── table.tsx ├── lib ├── createSquad.ts ├── hooks │ └── useSquadForm.ts ├── isProgram.ts ├── isPublickey.ts ├── transaction │ ├── decodeAndDeserialize.ts │ ├── getAccountsForSimulation.ts │ ├── importTransaction.ts │ └── simulateEncodedTransaction.ts └── utils.ts ├── middleware.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── tailwind.config.ts ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "react/no-children-prop": "warn" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .idea/ 39 | 40 | .env 41 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "trailingComma": "es5", 7 | "bracketSpacing": true 8 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Business Source License 1.1 2 | 3 | License text copyright © 2023 MariaDB plc, All Rights Reserved. 4 | “Business Source License” is a trademark of MariaDB plc. 5 | 6 | ----------------------------------------------------------------------------- 7 | 8 | Parameters 9 | 10 | Licensor: Selimor Investments Limited 11 | 12 | Licensed Work: Squads Multisig Program V4 13 | The Licensed Work is © 2023 Selimor Investments Limited 14 | 15 | Additional Use Grant: NONE 16 | 17 | Change Date: Jan 30, 2029 18 | 19 | Change License: Version 2 or later of the GNU General Public License as 20 | published by the Free Software Foundation. 21 | 22 | ----------------------------------------------------------------------------- 23 | 24 | Notice 25 | 26 | The Business Source License (this document, or the “License”) is not an Open 27 | Source license. However, the Licensed Work will eventually be made available 28 | under an Open Source License, as stated in this License. 29 | 30 | For more information on the use of the Business Source License for MariaDB 31 | products, please visit the MariaDB Business Source License FAQ at 32 | https://mariadb.com/bsl-faq-mariadb. 33 | 34 | For more information on the use of the Business Source License generally, 35 | please visit the Adopting and Developing Business Source License FAQ at 36 | https://mariadb.com/bsl-faq-adopting. 37 | 38 | ----------------------------------------------------------------------------- 39 | 40 | Business Source License 1.1 41 | 42 | Terms 43 | 44 | The Licensor hereby grants you the right to copy, modify, create derivative 45 | works, redistribute, and make non-production use of the Licensed Work. The 46 | Licensor may make an Additional Use Grant, above, permitting limited 47 | production use. 48 | 49 | Effective on the Change Date, or the fourth anniversary of the first publicly 50 | available distribution of a specific version of the Licensed Work under this 51 | License, whichever comes first, the Licensor hereby grants you rights under 52 | the terms of the Change License, and the rights granted in the paragraph 53 | above terminate. 54 | 55 | If your use of the Licensed Work does not comply with the requirements 56 | currently in effect as described in this License, you must purchase a 57 | commercial license from the Licensor, its affiliated entities, or authorized 58 | resellers, or you must refrain from using the Licensed Work. 59 | 60 | All copies of the original and modified Licensed Work, and derivative works 61 | of the Licensed Work, are subject to this License. This License applies 62 | separately for each version of the Licensed Work and the Change Date may vary 63 | for each version of the Licensed Work released by Licensor. 64 | 65 | You must conspicuously display this License on each original or modified copy 66 | of the Licensed Work. If you receive the Licensed Work in original or 67 | modified form from a third party, the terms and conditions set forth in this 68 | License apply to your use of that work. 69 | 70 | Any use of the Licensed Work in violation of this License will automatically 71 | terminate your rights under this License for the current and all other 72 | versions of the Licensed Work. 73 | 74 | This License does not grant you any right in any trademark or logo of 75 | Licensor or its affiliates (provided that you may use a trademark or logo of 76 | Licensor as expressly required by this License). 77 | 78 | TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON 79 | AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, 80 | EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF 81 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND 82 | TITLE. 83 | 84 | MariaDB hereby grants you permission to use this License’s text to license 85 | your works, and to refer to it using the trademark “Business Source License”, 86 | as long as you comply with the Covenants of Licensor below. 87 | 88 | Covenants of Licensor 89 | 90 | In consideration of the right to use this License’s text and the “Business 91 | Source License” name and trademark, Licensor covenants to MariaDB, and to all 92 | other recipients of the licensed work to be provided by Licensor: 93 | 94 | 1. To specify as the Change License the GPL Version 2.0 or any later version, 95 | or a license that is compatible with GPL Version 2.0 or a later version, 96 | where “compatible” means that software provided under the Change License can 97 | be included in a program with software provided under GPL Version 2.0 or a 98 | later version. Licensor may specify additional Change Licenses without 99 | limitation. 100 | 101 | 2. To either: (a) specify an additional grant of rights to use that does not 102 | impose any additional restriction on the right granted in this License, as 103 | the Additional Use Grant; or (b) insert the text “None”. 104 | 105 | 3. To specify a Change Date. 106 | 107 | 4. Not to modify this License in any other way. 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### This repo has been deprecated ### 2 | Please use https://github.com/Squads-Protocol/public-v4-client for the simplified repo 3 | 4 | # Squads V4 public UI 5 | 6 | This repository contains the code for a source available Squads V4 user interface. 7 | 8 | ## Usage on local device 9 | 10 | ### Requirements 11 | 12 | - [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 13 | - [NodeJS](https://nodejs.org/en/download) 14 | - [Yarn](https://classic.yarnpkg.com/lang/en/docs/install/#windows-stable) 15 | 16 | ### Installation 17 | 18 | First, clone this repository to a folder on your device. 19 | 20 | ``` 21 | git clone https://github.com/Squads-Protocol/squads-v4-public-ui.git . 22 | ``` 23 | 24 | Then, install the required dependencies and build the app. 25 | 26 | ``` 27 | yarn && yarn build 28 | ``` 29 | 30 | ### Start app locally 31 | 32 | ``` 33 | yarn start 34 | ``` 35 | 36 | ## Configuration 37 | 38 | There are multiple configuration options available which are stored in the cookies of the application, but can also be set via the UI. 39 | 40 | - RPC url 41 | 42 | Default: https://api.mainnet-beta.solana.com 43 | 44 | Cookie name: x-rpc-url 45 | 46 | - Multisig address 47 | 48 | Cookie name: x-multisig 49 | 50 | - Vault Index 51 | 52 | Cookie name: x-vault-index 53 | 54 | ## Disclaimer 55 | 56 | Use this code at your own risk. 57 | 58 | By using the provided code, you agree that the maintainer of this repository will not be help responsible for any type of issue that may have occured. 59 | -------------------------------------------------------------------------------- /app/(app)/config/page.tsx: -------------------------------------------------------------------------------- 1 | import AddMemberInput from "@/components/AddMemberInput"; 2 | import ChangeThresholdInput from "@/components/ChangeThresholdInput"; 3 | import ChangeUpgradeAuthorityInput from "@/components/ChangeUpgradeAuthorityInput"; 4 | import RemoveMemberButton from "@/components/RemoveMemberButton"; 5 | import { 6 | Card, 7 | CardContent, 8 | CardDescription, 9 | CardHeader, 10 | CardTitle, 11 | } from "@/components/ui/card"; 12 | import { Connection, PublicKey, clusterApiUrl } from "@solana/web3.js"; 13 | import * as multisig from "@sqds/multisig"; 14 | import { cookies, headers } from "next/headers"; 15 | const ConfigurationPage = async () => { 16 | const rpcUrl = headers().get("x-rpc-url"); 17 | 18 | const connection = new Connection(rpcUrl || clusterApiUrl("mainnet-beta")); 19 | const multisigCookie = headers().get("x-multisig"); 20 | const multisigPda = new PublicKey(multisigCookie!); 21 | const vaultIndex = Number(headers().get("x-vault-index")); 22 | const programIdCookie = cookies().get("x-program-id")?.value; 23 | const programId = programIdCookie 24 | ? new PublicKey(programIdCookie!) 25 | : multisig.PROGRAM_ID; 26 | 27 | const multisigInfo = await multisig.accounts.Multisig.fromAccountAddress( 28 | connection, 29 | multisigPda 30 | ); 31 | return ( 32 |
33 |

Multisig Configuration

34 | 35 | 36 | Members 37 | 38 | List of members in the multisig as well as their permissions. 39 | 40 | 41 | 42 |
43 | {multisigInfo.members.map((member) => ( 44 |
45 |
46 |
47 |

48 | Public Key: {member.key.toBase58()} 49 |

50 |

51 | Permission Mask: 52 | {member.permissions.mask.toString()} 53 |

54 |
55 |
56 | 69 |
70 |
71 |
72 |
73 | ))} 74 |
75 |
76 |
77 |
78 | 79 | 80 | Add Member 81 | Add a member to the Multisig 82 | 83 | 84 | 94 | 95 | 96 | 97 | 98 | Change Threshold 99 | 100 | Change the threshold required to execute a multisig transaction. 101 | 102 | 103 | 104 | 114 | 115 | 116 |
117 |
118 | 119 | 120 | Change program Upgrade authority 121 | 122 | Change the upgrade authority of one of your programs. 123 | 124 | 125 | 126 | 137 | 138 | 139 |
140 |
141 | ); 142 | }; 143 | 144 | export default ConfigurationPage; 145 | -------------------------------------------------------------------------------- /app/(app)/create/page.tsx: -------------------------------------------------------------------------------- 1 | import CreateSquadForm from "@/components/CreateSquadForm"; 2 | import { Card, CardContent } from "@/components/ui/card"; 3 | import { PROGRAM_ID } from "@sqds/multisig"; 4 | import { cookies, headers } from "next/headers"; 5 | 6 | export default async function CreateSquad() { 7 | const rpcUrl = headers().get("x-rpc-url"); 8 | const programId = cookies().get("x-program-id")?.value; 9 | 10 | return ( 11 |
12 |
13 |

Create a Squad

14 |

15 | Create a Squad and set it as your default account. 16 |

17 |
18 | 19 | 20 | 24 | 25 | 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/(app)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { headers } from "next/headers"; 2 | import Link from "next/link"; 3 | import Image from "next/image"; 4 | import { Connection, PublicKey, clusterApiUrl } from "@solana/web3.js"; 5 | import * as multisig from "@sqds/multisig"; 6 | import { Toaster } from "@/components/ui/sonner"; 7 | import ConnectWallet from "@/components/ConnectWalletButton"; 8 | import { 9 | LucideHome, 10 | ArrowDownUp, 11 | Users, 12 | Settings, 13 | CheckSquare, 14 | AlertTriangle, 15 | } from "lucide-react"; 16 | import RenderMultisigRoute from "@/components/RenderMultisigRoute"; 17 | 18 | const AppLayout = async ({ children }: { children: React.ReactNode }) => { 19 | const tabs = [ 20 | { 21 | name: "Home", 22 | icon: , 23 | route: "/", 24 | }, 25 | { 26 | name: "Transactions", 27 | icon: , 28 | route: "/transactions", 29 | }, 30 | { 31 | name: "Configuration", 32 | icon: , 33 | route: "/config", 34 | }, 35 | { 36 | name: "Settings", 37 | icon: , 38 | route: "/settings", 39 | }, 40 | ]; 41 | 42 | const headersList = headers(); 43 | 44 | const path = headersList.get("x-pathname"); 45 | const multisigCookie = headersList.get("x-multisig"); 46 | const multisig = await isValidPublicKey(multisigCookie!); 47 | 48 | return ( 49 | <> 50 |
51 | 98 | 99 | 120 | 121 | 122 |
123 | , 128 | success: , 129 | }} 130 | /> 131 | 132 | ); 133 | }; 134 | 135 | export default AppLayout; 136 | 137 | const isValidPublicKey = async (multisigString: string) => { 138 | try { 139 | const multisigPubkey = new PublicKey(multisigString); // This will throw an error if the string is not a valid public key 140 | const rpcUrl = headers().get("x-rpc-url"); 141 | const connection = new Connection(rpcUrl || clusterApiUrl("mainnet-beta")); 142 | await multisig.accounts.Multisig.fromAccountAddress( 143 | connection, 144 | multisigPubkey 145 | ); 146 | return true; 147 | } catch (e) { 148 | return false; 149 | } 150 | }; 151 | -------------------------------------------------------------------------------- /app/(app)/page.tsx: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey, clusterApiUrl } from "@solana/web3.js"; 2 | import { cookies, headers } from "next/headers"; 3 | import * as multisig from "@sqds/multisig"; 4 | import { TokenList } from "@/components/TokenList"; 5 | import { VaultDisplayer } from "@/components/VaultDisplayer"; 6 | 7 | export default async function Home() { 8 | const rpcUrl = headers().get("x-rpc-url"); 9 | 10 | const connection = new Connection(rpcUrl || clusterApiUrl("mainnet-beta")); 11 | const multisigCookie = headers().get("x-multisig"); 12 | const multisigPda = new PublicKey(multisigCookie!); 13 | const vaultIndex = Number(headers().get("x-vault-index")); 14 | const programIdCookie = cookies().get("x-program-id")?.value; 15 | const programId = programIdCookie 16 | ? new PublicKey(programIdCookie!) 17 | : multisig.PROGRAM_ID; 18 | 19 | const multisigVault = multisig.getVaultPda({ 20 | multisigPda, 21 | index: vaultIndex || 0, 22 | programId: programId ? programId : multisig.PROGRAM_ID, 23 | })[0]; 24 | 25 | const solBalance = await connection.getBalance(multisigVault); 26 | 27 | const tokensInWallet = await connection.getParsedTokenAccountsByOwner( 28 | multisigVault, 29 | { 30 | programId: new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"), 31 | } 32 | ); 33 | 34 | return ( 35 |
36 |
37 |

Overview

38 | 39 | 44 | 52 |
53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /app/(app)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import SetProgramIdInput from "@/components/SetProgramIdInput"; 2 | import SetRpcUrlInput from "@/components/SetRpcUrlnput"; 3 | import { 4 | Card, 5 | CardContent, 6 | CardDescription, 7 | CardHeader, 8 | CardTitle, 9 | } from "@/components/ui/card"; 10 | 11 | const SettingsPage = () => { 12 | return ( 13 |
14 |

Settings

15 |
16 | 17 | 18 | RPC Url 19 | 20 | Change the default RPC Url for this app. 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Program ID 30 | Change the targeted program ID. 31 | 32 | 33 | 34 | 35 | 36 |
37 |
38 | ); 39 | }; 40 | 41 | export default SettingsPage; 42 | -------------------------------------------------------------------------------- /app/(app)/transactions/page.tsx: -------------------------------------------------------------------------------- 1 | import * as multisig from "@sqds/multisig"; 2 | import { cookies, headers } from "next/headers"; 3 | import { Connection, PublicKey, clusterApiUrl } from "@solana/web3.js"; 4 | import { 5 | Table, 6 | TableCaption, 7 | TableHead, 8 | TableHeader, 9 | TableRow, 10 | } from "@/components/ui/table"; 11 | import { 12 | Pagination, 13 | PaginationContent, 14 | PaginationItem, 15 | PaginationNext, 16 | PaginationPrevious, 17 | } from "@/components/ui/pagination"; 18 | import { Suspense } from "react"; 19 | import CreateTransaction from "@/components/CreateTransactionButton"; 20 | import TransactionTable from "@/components/TransactionTable"; 21 | 22 | const TRANSACTIONS_PER_PAGE = 10; 23 | 24 | interface ActionButtonsProps { 25 | rpcUrl: string; 26 | multisigPda: string; 27 | transactionIndex: number; 28 | proposalStatus: string; 29 | programId: PublicKey; 30 | } 31 | 32 | export default async function TransactionsPage({ 33 | params, 34 | searchParams, 35 | }: { 36 | params: {}; 37 | searchParams: { page: string }; 38 | }) { 39 | const page = searchParams.page ? parseInt(searchParams.page) : 1; 40 | const rpcUrl = headers().get("x-rpc-url"); 41 | const connection = new Connection( 42 | rpcUrl || clusterApiUrl("mainnet-beta"), 43 | "confirmed" 44 | ); 45 | const multisigCookie = cookies().get("x-multisig")?.value; 46 | const multisigPda = new PublicKey(multisigCookie!); 47 | const vaultIndex = Number(headers().get("x-vault-index")); 48 | const programIdCookie = cookies().get("x-program-id") 49 | ? cookies().get("x-program-id")?.value 50 | : multisig.PROGRAM_ID.toString(); 51 | const programId = programIdCookie 52 | ? new PublicKey(programIdCookie!) 53 | : multisig.PROGRAM_ID; 54 | 55 | const multisigInfo = await multisig.accounts.Multisig.fromAccountAddress( 56 | connection, 57 | multisigPda 58 | ); 59 | 60 | const totalTransactions = Number(multisigInfo.transactionIndex); 61 | const totalPages = Math.ceil(totalTransactions / TRANSACTIONS_PER_PAGE); 62 | 63 | /* 64 | if (page > totalPages) { 65 | redirect(`/transactions?page=0`); 66 | } 67 | */ 68 | 69 | const startIndex = totalTransactions - (page - 1) * TRANSACTIONS_PER_PAGE; 70 | const endIndex = Math.max(startIndex - TRANSACTIONS_PER_PAGE + 1, 1); 71 | 72 | const latestTransactions = await Promise.all( 73 | Array.from({ length: startIndex - endIndex + 1 }, (_, i) => { 74 | const index = BigInt(startIndex - i); 75 | return fetchTransactionData(connection, multisigPda, index, programId); 76 | }) 77 | ); 78 | 79 | const transactions = latestTransactions.map((transaction) => { 80 | return { 81 | ...transaction, 82 | transactionPda: transaction.transactionPda[0].toBase58(), 83 | }; 84 | }); 85 | 86 | return ( 87 |
88 |
89 |

Transactions

90 | 96 |
97 | 98 | Loading...
}> 99 | 100 | A list of your recent transactions. 101 | 102 | Page: {totalPages > 0 ? page + 1 : 0} of {totalPages} 103 | 104 | 105 | 106 | 107 | Index 108 | Public Key 109 | Proposal Status 110 | Actions 111 | 112 | 113 | Loading...}> 114 | 120 | 121 |
122 | 123 | 124 | 125 | 126 | {page > 1 && ( 127 | 128 | 129 | 130 | )} 131 | {page < totalPages && ( 132 | 133 | 134 | 135 | )} 136 | 137 | 138 | 139 | ); 140 | } 141 | 142 | async function fetchTransactionData( 143 | connection: Connection, 144 | multisigPda: PublicKey, 145 | index: bigint, 146 | programId: PublicKey 147 | ) { 148 | const transactionPda = multisig.getTransactionPda({ 149 | multisigPda, 150 | index, 151 | programId, 152 | }); 153 | const proposalPda = multisig.getProposalPda({ 154 | multisigPda, 155 | transactionIndex: index, 156 | programId, 157 | }); 158 | 159 | let proposal; 160 | try { 161 | proposal = await multisig.accounts.Proposal.fromAccountAddress( 162 | connection, 163 | proposalPda[0] 164 | ); 165 | } catch (error) { 166 | proposal = null; 167 | } 168 | 169 | return { transactionPda, proposal, index }; 170 | } 171 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Squads-Protocol/squads-v4-public-ui/03cdc1771596bfeedf7ed31643f6cea5c7885b99/app/favicon.ico -------------------------------------------------------------------------------- /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: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import { Wallet } from "@/components/Wallet"; 5 | 6 | const inter = Inter({ subsets: ["latin"] }); 7 | 8 | export const metadata: Metadata = { 9 | title: "Squads Simplified", 10 | description: 11 | "Squads v4 program user interface.", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: { 17 | children: React.ReactNode; 18 | }) { 19 | return ( 20 | 21 | 22 | {children} 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /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": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/AddMemberInput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from "next/navigation"; 3 | import { Button } from "./ui/button"; 4 | import { Input } from "./ui/input"; 5 | import { useWallet } from "@solana/wallet-adapter-react"; 6 | import { useState } from "react"; 7 | import { useWalletModal } from "@solana/wallet-adapter-react-ui"; 8 | import * as multisig from "@sqds/multisig"; 9 | import { 10 | Connection, 11 | PublicKey, 12 | TransactionMessage, 13 | VersionedTransaction, 14 | } from "@solana/web3.js"; 15 | import { toast } from "sonner"; 16 | import { isPublickey } from "@/lib/isPublickey"; 17 | 18 | type AddMemberInputProps = { 19 | multisigPda: string; 20 | transactionIndex: number; 21 | rpcUrl: string; 22 | programId: string; 23 | }; 24 | 25 | const AddMemberInput = ({ 26 | multisigPda, 27 | transactionIndex, 28 | rpcUrl, 29 | programId, 30 | }: AddMemberInputProps) => { 31 | const [member, setMember] = useState(""); 32 | const wallet = useWallet(); 33 | const walletModal = useWalletModal(); 34 | const router = useRouter(); 35 | 36 | const bigIntTransactionIndex = BigInt(transactionIndex); 37 | const connection = new Connection(rpcUrl, { commitment: "confirmed" }); 38 | 39 | const addMember = async () => { 40 | if (!wallet.publicKey) { 41 | walletModal.setVisible(true); 42 | return; 43 | } 44 | 45 | const addMemberIx = multisig.instructions.configTransactionCreate({ 46 | multisigPda: new PublicKey(multisigPda), 47 | actions: [ 48 | { 49 | __kind: "AddMember", 50 | newMember: { 51 | key: new PublicKey(member), 52 | permissions: { 53 | mask: 7, 54 | }, 55 | }, 56 | }, 57 | ], 58 | creator: wallet.publicKey, 59 | transactionIndex: bigIntTransactionIndex, 60 | rentPayer: wallet.publicKey, 61 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 62 | }); 63 | const proposalIx = multisig.instructions.proposalCreate({ 64 | multisigPda: new PublicKey(multisigPda), 65 | creator: wallet.publicKey, 66 | isDraft: false, 67 | transactionIndex: bigIntTransactionIndex, 68 | rentPayer: wallet.publicKey, 69 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 70 | }); 71 | const approveIx = multisig.instructions.proposalApprove({ 72 | multisigPda: new PublicKey(multisigPda), 73 | member: wallet.publicKey, 74 | transactionIndex: bigIntTransactionIndex, 75 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 76 | }); 77 | 78 | const message = new TransactionMessage({ 79 | instructions: [addMemberIx, proposalIx, approveIx], 80 | payerKey: wallet.publicKey, 81 | recentBlockhash: (await connection.getLatestBlockhash()).blockhash, 82 | }).compileToV0Message(); 83 | 84 | const transaction = new VersionedTransaction(message); 85 | 86 | const signature = await wallet.sendTransaction(transaction, connection, { 87 | skipPreflight: true, 88 | }); 89 | console.log("Transaction signature", signature); 90 | toast.loading("Confirming...", { 91 | id: "transaction", 92 | }); 93 | await connection.getSignatureStatuses([signature]); 94 | await new Promise((resolve) => setTimeout(resolve, 1000)); 95 | router.refresh(); 96 | }; 97 | return ( 98 |
99 | setMember(e.target.value)} 102 | className="mb-3" 103 | /> 104 | 117 |
118 | ); 119 | }; 120 | 121 | export default AddMemberInput; 122 | -------------------------------------------------------------------------------- /components/ApproveButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | Connection, 4 | PublicKey, 5 | Transaction, 6 | clusterApiUrl, 7 | } from "@solana/web3.js"; 8 | import { Button } from "./ui/button"; 9 | import * as multisig from "@sqds/multisig"; 10 | import { useWallet } from "@solana/wallet-adapter-react"; 11 | import { useWalletModal } from "@solana/wallet-adapter-react-ui"; 12 | import { toast } from "sonner"; 13 | import { useRouter } from "next/navigation"; 14 | 15 | type ApproveButtonProps = { 16 | rpcUrl: string; 17 | multisigPda: string; 18 | transactionIndex: number; 19 | proposalStatus: string; 20 | programId: string; 21 | }; 22 | 23 | const ApproveButton = ({ 24 | rpcUrl, 25 | multisigPda, 26 | transactionIndex, 27 | proposalStatus, 28 | programId, 29 | }: ApproveButtonProps) => { 30 | const wallet = useWallet(); 31 | const walletModal = useWalletModal(); 32 | const router = useRouter(); 33 | const validKinds = [ 34 | "Rejected", 35 | "Approved", 36 | "Executing", 37 | "Executed", 38 | "Cancelled", 39 | ]; 40 | const isKindValid = validKinds.includes(proposalStatus || "None"); 41 | const connection = new Connection(rpcUrl || clusterApiUrl("mainnet-beta"), { 42 | commitment: "confirmed", 43 | }); 44 | 45 | const approveProposal = async () => { 46 | if (!wallet.publicKey) { 47 | walletModal.setVisible(true); 48 | return; 49 | } 50 | let bigIntTransactionIndex = BigInt(transactionIndex); 51 | const transaction = new Transaction(); 52 | if (proposalStatus === "None") { 53 | const createProposalInstruction = multisig.instructions.proposalCreate({ 54 | multisigPda: new PublicKey(multisigPda), 55 | creator: wallet.publicKey, 56 | isDraft: false, 57 | transactionIndex: bigIntTransactionIndex, 58 | rentPayer: wallet.publicKey, 59 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 60 | }); 61 | transaction.add(createProposalInstruction); 62 | } 63 | if (proposalStatus == "Draft") { 64 | const activateProposalInstruction = 65 | multisig.instructions.proposalActivate({ 66 | multisigPda: new PublicKey(multisigPda), 67 | member: wallet.publicKey, 68 | transactionIndex: bigIntTransactionIndex, 69 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 70 | }); 71 | transaction.add(activateProposalInstruction); 72 | } 73 | const approveProposalInstruction = multisig.instructions.proposalApprove({ 74 | multisigPda: new PublicKey(multisigPda), 75 | member: wallet.publicKey, 76 | transactionIndex: bigIntTransactionIndex, 77 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 78 | }); 79 | transaction.add(approveProposalInstruction); 80 | const signature = await wallet.sendTransaction(transaction, connection, { 81 | skipPreflight: true, 82 | }); 83 | console.log("Transaction signature", signature); 84 | toast.loading("Confirming...", { 85 | id: "transaction", 86 | }); 87 | await connection.getSignatureStatuses([signature]); 88 | await new Promise((resolve) => setTimeout(resolve, 1000)); 89 | router.refresh(); 90 | }; 91 | return ( 92 | 106 | ); 107 | }; 108 | 109 | export default ApproveButton; 110 | -------------------------------------------------------------------------------- /components/ChangeThresholdInput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from "next/navigation"; 3 | import { Button } from "./ui/button"; 4 | import { Input } from "./ui/input"; 5 | import { useWallet } from "@solana/wallet-adapter-react"; 6 | import { useState } from "react"; 7 | import { useWalletModal } from "@solana/wallet-adapter-react-ui"; 8 | import * as multisig from "@sqds/multisig"; 9 | import { 10 | Connection, 11 | PublicKey, 12 | TransactionMessage, 13 | VersionedTransaction, 14 | } from "@solana/web3.js"; 15 | import { toast } from "sonner"; 16 | 17 | type ChangeThresholdInputProps = { 18 | multisigPda: string; 19 | transactionIndex: number; 20 | rpcUrl: string; 21 | programId: string; 22 | }; 23 | 24 | const ChangeThresholdInput = ({ 25 | multisigPda, 26 | transactionIndex, 27 | rpcUrl, 28 | programId, 29 | }: ChangeThresholdInputProps) => { 30 | const [threshold, setThreshold] = useState(""); 31 | const wallet = useWallet(); 32 | const walletModal = useWalletModal(); 33 | const router = useRouter(); 34 | 35 | const bigIntTransactionIndex = BigInt(transactionIndex); 36 | const connection = new Connection(rpcUrl, { commitment: "confirmed" }); 37 | 38 | const changeThreshold = async () => { 39 | if (!wallet.publicKey) { 40 | walletModal.setVisible(true); 41 | return; 42 | } 43 | 44 | const changeThresholdIx = multisig.instructions.configTransactionCreate({ 45 | multisigPda: new PublicKey(multisigPda), 46 | actions: [ 47 | { 48 | __kind: "ChangeThreshold", 49 | newThreshold: parseInt(threshold), 50 | }, 51 | ], 52 | creator: wallet.publicKey, 53 | transactionIndex: bigIntTransactionIndex, 54 | rentPayer: wallet.publicKey, 55 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 56 | }); 57 | const proposalIx = multisig.instructions.proposalCreate({ 58 | multisigPda: new PublicKey(multisigPda), 59 | creator: wallet.publicKey, 60 | isDraft: false, 61 | transactionIndex: bigIntTransactionIndex, 62 | rentPayer: wallet.publicKey, 63 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 64 | }); 65 | const approveIx = multisig.instructions.proposalApprove({ 66 | multisigPda: new PublicKey(multisigPda), 67 | member: wallet.publicKey, 68 | transactionIndex: bigIntTransactionIndex, 69 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 70 | }); 71 | 72 | const message = new TransactionMessage({ 73 | instructions: [changeThresholdIx, proposalIx, approveIx], 74 | payerKey: wallet.publicKey, 75 | recentBlockhash: (await connection.getLatestBlockhash()).blockhash, 76 | }).compileToV0Message(); 77 | 78 | const transaction = new VersionedTransaction(message); 79 | 80 | const signature = await wallet.sendTransaction(transaction, connection, { 81 | skipPreflight: true, 82 | }); 83 | console.log("Transaction signature", signature); 84 | toast.loading("Confirming...", { 85 | id: "transaction", 86 | }); 87 | await connection.getSignatureStatuses([signature]); 88 | await new Promise((resolve) => setTimeout(resolve, 1000)); 89 | router.refresh(); 90 | }; 91 | return ( 92 |
93 | setThreshold(e.target.value)} 97 | className="mb-3" 98 | /> 99 | 112 |
113 | ); 114 | }; 115 | 116 | export default ChangeThresholdInput; 117 | -------------------------------------------------------------------------------- /components/ChangeUpgradeAuthorityInput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from "next/navigation"; 3 | import { Button } from "./ui/button"; 4 | import { Input } from "./ui/input"; 5 | import { useWallet } from "@solana/wallet-adapter-react"; 6 | import { useState } from "react"; 7 | import { useWalletModal } from "@solana/wallet-adapter-react-ui"; 8 | import * as multisig from "@sqds/multisig"; 9 | import { 10 | AccountMeta, 11 | Connection, 12 | PublicKey, 13 | TransactionInstruction, 14 | TransactionMessage, 15 | VersionedTransaction, 16 | } from "@solana/web3.js"; 17 | import { toast } from "sonner"; 18 | import { isPublickey } from "@/lib/isPublickey"; 19 | 20 | type ChangeUpgradeAuthorityInputProps = { 21 | multisigPda: string; 22 | transactionIndex: number; 23 | rpcUrl: string; 24 | vaultIndex: number; 25 | globalProgramId: string; 26 | }; 27 | 28 | const ChangeUpgradeAuthorityInput = ({ 29 | multisigPda, 30 | transactionIndex, 31 | rpcUrl, 32 | vaultIndex, 33 | globalProgramId, 34 | }: ChangeUpgradeAuthorityInputProps) => { 35 | const [programId, setProgramId] = useState(""); 36 | const [newAuthority, setNewAuthority] = useState(""); 37 | const wallet = useWallet(); 38 | const walletModal = useWalletModal(); 39 | const router = useRouter(); 40 | 41 | const bigIntTransactionIndex = BigInt(transactionIndex); 42 | const connection = new Connection(rpcUrl, { commitment: "confirmed" }); 43 | 44 | const vaultAddress = multisig.getVaultPda({ 45 | index: vaultIndex, 46 | multisigPda: new PublicKey(multisigPda), 47 | programId: globalProgramId 48 | ? new PublicKey(globalProgramId) 49 | : multisig.PROGRAM_ID, 50 | })[0]; 51 | 52 | const changeUpgradeAuth = async () => { 53 | if (!wallet.publicKey) { 54 | walletModal.setVisible(true); 55 | return; 56 | } 57 | 58 | const upgradeData = Buffer.alloc(4); 59 | upgradeData.writeInt32LE(4, 0); 60 | 61 | const [programDataAddress] = PublicKey.findProgramAddressSync( 62 | [new PublicKey(programId).toBuffer()], 63 | new PublicKey("BPFLoaderUpgradeab1e11111111111111111111111") 64 | ); 65 | const keys: AccountMeta[] = [ 66 | { 67 | pubkey: programDataAddress, 68 | isWritable: true, 69 | isSigner: false, 70 | }, 71 | { 72 | pubkey: vaultAddress, 73 | isWritable: false, 74 | isSigner: true, 75 | }, 76 | { 77 | pubkey: new PublicKey(newAuthority), 78 | isWritable: false, 79 | isSigner: false, 80 | }, 81 | ]; 82 | 83 | const blockhash = (await connection.getLatestBlockhash()).blockhash; 84 | 85 | const transactionMessage = new TransactionMessage({ 86 | instructions: [ 87 | new TransactionInstruction({ 88 | programId: new PublicKey( 89 | "BPFLoaderUpgradeab1e11111111111111111111111" 90 | ), 91 | data: upgradeData, 92 | keys, 93 | }), 94 | ], 95 | payerKey: wallet.publicKey, 96 | recentBlockhash: blockhash, 97 | }); 98 | 99 | const transactionIndexBN = BigInt(transactionIndex); 100 | 101 | const multisigTransactionIx = multisig.instructions.vaultTransactionCreate({ 102 | multisigPda: new PublicKey(multisigPda), 103 | creator: wallet.publicKey, 104 | ephemeralSigners: 0, 105 | transactionMessage, 106 | transactionIndex: transactionIndexBN, 107 | addressLookupTableAccounts: [], 108 | rentPayer: wallet.publicKey, 109 | vaultIndex: vaultIndex, 110 | programId: globalProgramId 111 | ? new PublicKey(globalProgramId) 112 | : multisig.PROGRAM_ID, 113 | }); 114 | const proposalIx = multisig.instructions.proposalCreate({ 115 | multisigPda: new PublicKey(multisigPda), 116 | creator: wallet.publicKey, 117 | isDraft: false, 118 | transactionIndex: bigIntTransactionIndex, 119 | rentPayer: wallet.publicKey, 120 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 121 | }); 122 | const approveIx = multisig.instructions.proposalApprove({ 123 | multisigPda: new PublicKey(multisigPda), 124 | member: wallet.publicKey, 125 | transactionIndex: bigIntTransactionIndex, 126 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 127 | }); 128 | 129 | const message = new TransactionMessage({ 130 | instructions: [multisigTransactionIx, proposalIx, approveIx], 131 | payerKey: wallet.publicKey, 132 | recentBlockhash: (await connection.getLatestBlockhash()).blockhash, 133 | }).compileToV0Message(); 134 | 135 | const transaction = new VersionedTransaction(message); 136 | 137 | const signature = await wallet.sendTransaction(transaction, connection, { 138 | skipPreflight: true, 139 | }); 140 | console.log("Transaction signature", signature); 141 | toast.loading("Confirming...", { 142 | id: "transaction", 143 | }); 144 | await connection.getSignatureStatuses([signature]); 145 | await new Promise((resolve) => setTimeout(resolve, 1000)); 146 | router.refresh(); 147 | }; 148 | return ( 149 |
150 | setProgramId(e.target.value)} 154 | className="mb-3" 155 | /> 156 | setNewAuthority(e.target.value)} 160 | className="mb-3" 161 | /> 162 | 175 |
176 | ); 177 | }; 178 | 179 | export default ChangeUpgradeAuthorityInput; 180 | -------------------------------------------------------------------------------- /components/ConnectWalletButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useWalletModal } from "@solana/wallet-adapter-react-ui"; 3 | import { Button } from "./ui/button"; 4 | import { useWallet } from "@solana/wallet-adapter-react"; 5 | 6 | const ConnectWallet = () => { 7 | const modal = useWalletModal(); 8 | const { publicKey, disconnect } = useWallet(); 9 | return ( 10 |
11 | {!publicKey ? ( 12 | 15 | ) : ( 16 | 19 | )} 20 |
21 | ); 22 | }; 23 | 24 | export default ConnectWallet; 25 | -------------------------------------------------------------------------------- /components/CreateSquadForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Button } from "./ui/button"; 3 | import { Input } from "./ui/input"; 4 | import { Member, createMultisig } from "@/lib/createSquad"; 5 | import { Connection, Keypair, PublicKey, clusterApiUrl } from "@solana/web3.js"; 6 | import { useWallet } from "@solana/wallet-adapter-react"; 7 | import { 8 | CheckSquare, 9 | Copy, 10 | ExternalLink, 11 | PlusCircleIcon, 12 | XIcon, 13 | } from "lucide-react"; 14 | import { 15 | Select, 16 | SelectContent, 17 | SelectGroup, 18 | SelectItem, 19 | SelectTrigger, 20 | SelectValue, 21 | } from "./ui/select"; 22 | import { toast } from "sonner"; 23 | import { useRouter } from "next/navigation"; 24 | import { isPublickey } from "@/lib/isPublickey"; 25 | import { ValidationRules, useSquadForm } from "@/lib/hooks/useSquadForm"; 26 | import Link from "next/link"; 27 | 28 | interface MemberAddresses { 29 | count: number; 30 | memberData: Member[]; 31 | } 32 | 33 | interface CreateSquadFormData { 34 | members: MemberAddresses; 35 | threshold: number; 36 | rentCollector: string; 37 | configAuthority: string; 38 | createKey: string; 39 | } 40 | 41 | export default function CreateSquadForm({ 42 | rpc, 43 | programId, 44 | }: { 45 | rpc: string; 46 | programId: string; 47 | }) { 48 | const router = useRouter(); 49 | const { publicKey, connected, sendTransaction } = useWallet(); 50 | 51 | const connection = new Connection(rpc || clusterApiUrl("mainnet-beta")); 52 | const validationRules = getValidationRules(); 53 | 54 | const { formState, handleChange, handleAddMember, onSubmit } = useSquadForm<{ 55 | signature: string; 56 | multisig: string; 57 | }>( 58 | { 59 | threshold: 1, 60 | rentCollector: "", 61 | configAuthority: "", 62 | createKey: "", 63 | members: { 64 | count: 0, 65 | memberData: [], 66 | }, 67 | }, 68 | validationRules 69 | ); 70 | 71 | async function submitHandler() { 72 | if (!connected) throw new Error("Please connect your wallet."); 73 | try { 74 | const createKey = Keypair.generate(); 75 | 76 | const { transaction, multisig } = await createMultisig( 77 | connection, 78 | publicKey!, 79 | formState.values.members.memberData, 80 | formState.values.threshold, 81 | createKey.publicKey, 82 | formState.values.rentCollector, 83 | formState.values.configAuthority, 84 | programId 85 | ); 86 | 87 | const signature = await sendTransaction(transaction, connection, { 88 | skipPreflight: true, 89 | signers: [createKey], 90 | }); 91 | console.log("Transaction signature", signature); 92 | toast.loading("Confirming...", { 93 | id: "create", 94 | }); 95 | 96 | let sent = false; 97 | const maxAttempts = 10; 98 | const delayMs = 1000; 99 | for (let attempt = 0; attempt <= maxAttempts && !sent; attempt++) { 100 | const status = await connection.getSignatureStatus(signature); 101 | if (status?.value?.confirmationStatus === "confirmed") { 102 | await new Promise((resolve) => setTimeout(resolve, delayMs)); 103 | sent = true; 104 | } else { 105 | await new Promise((resolve) => setTimeout(resolve, delayMs)); 106 | } 107 | } 108 | 109 | document.cookie = `x-multisig=${multisig.toBase58()}; path=/`; 110 | 111 | return { signature, multisig: multisig.toBase58() }; 112 | } catch (error: any) { 113 | console.error(error); 114 | return error; 115 | } finally { 116 | await new Promise((resolve) => setTimeout(resolve, 5000)); 117 | router.refresh(); 118 | } 119 | } 120 | 121 | return ( 122 | <> 123 |
124 |
125 | 128 | {formState.values.members.memberData.map( 129 | (member: Member, i: number) => ( 130 |
131 |
132 | { 136 | handleChange("members", { 137 | count: formState.values.members.count, 138 | memberData: formState.values.members.memberData.map( 139 | (member: Member, index: number) => { 140 | if (index === i) { 141 | let newKey = null; 142 | try { 143 | if ( 144 | e.target.value && 145 | PublicKey.isOnCurve(e.target.value) 146 | ) { 147 | newKey = new PublicKey(e.target.value); 148 | } 149 | } catch (error) { 150 | console.error( 151 | "Invalid public key input:", 152 | error 153 | ); 154 | } 155 | return { 156 | ...member, 157 | key: newKey, 158 | }; 159 | } 160 | return member; 161 | } 162 | ), 163 | }); 164 | }} 165 | /> 166 | {i > 0 && ( 167 | { 169 | handleChange("members", { 170 | count: formState.values.members.count, 171 | memberData: 172 | formState.values.members.memberData.filter( 173 | (_: Member, index: number) => index !== i 174 | ), 175 | }); 176 | }} 177 | className="absolute inset-y-3 right-2 w-4 h-4 text-zinc-400 hover:text-zinc-600" 178 | /> 179 | )} 180 |
181 | 215 |
216 | ) 217 | )} 218 | 225 | {formState.errors.members && ( 226 |
227 | {formState.errors.members} 228 |
229 | )} 230 |
231 |
232 | 235 | 240 | handleChange("threshold", parseInt(e.target.value)) 241 | } 242 | className="" 243 | /> 244 | {formState.errors.threshold && ( 245 |
246 | {formState.errors.threshold} 247 |
248 | )} 249 |
250 |
251 | 254 | handleChange("rentCollector", e.target.value)} 259 | className="" 260 | /> 261 | {formState.errors.rentCollector && ( 262 |
263 | {formState.errors.rentCollector} 264 |
265 | )} 266 |
267 |
268 | 271 | handleChange("configAuthority", e.target.value)} 276 | className="" 277 | /> 278 | {formState.errors.configAuthority && ( 279 |
280 | {formState.errors.configAuthority} 281 |
282 | )} 283 |
284 |
285 | 334 | 335 | ); 336 | } 337 | 338 | function getValidationRules(): ValidationRules { 339 | return { 340 | threshold: async (value: number) => { 341 | if (value < 1) return "Threshold must be greater than 0"; 342 | return null; 343 | }, 344 | rentCollector: async (value: string) => { 345 | const valid = isPublickey(value); 346 | if (!valid) return "Rent collector must be a valid public key"; 347 | return null; 348 | }, 349 | configAuthority: async (value: string) => { 350 | const valid = isPublickey(value); 351 | if (!valid) return "Config authority must be a valid public key"; 352 | return null; 353 | }, 354 | members: async (value: { count: number; memberData: Member[] }) => { 355 | if (value.count < 1) return "At least one member is required"; 356 | 357 | const valid = await Promise.all( 358 | value.memberData.map(async (member) => { 359 | if (member.key == null) return "Invalid Member Key"; 360 | const valid = isPublickey(member.key.toBase58()); 361 | if (!valid) return "Invalid Member Key"; 362 | return null; 363 | }) 364 | ); 365 | 366 | if (valid.includes("Invalid Member Key")) { 367 | let index = valid.findIndex((v) => v === "Invalid Member Key"); 368 | return `Member ${index + 1} is invalid`; 369 | } 370 | 371 | return null; 372 | }, 373 | }; 374 | } 375 | -------------------------------------------------------------------------------- /components/CreateTransactionButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogHeader, 7 | DialogTitle, 8 | DialogTrigger, 9 | } from "@/components/ui/dialog"; 10 | import * as bs58 from "bs58"; 11 | import { Button } from "./ui/button"; 12 | import { useState } from "react"; 13 | import * as multisig from "@sqds/multisig"; 14 | import { useWallet } from "@solana/wallet-adapter-react"; 15 | import { 16 | Connection, 17 | Message, 18 | PublicKey, 19 | TransactionInstruction, 20 | clusterApiUrl, 21 | } from "@solana/web3.js"; 22 | import { Input } from "./ui/input"; 23 | import { toast } from "sonner"; 24 | import { simulateEncodedTransaction } from "@/lib/transaction/simulateEncodedTransaction"; 25 | import { importTransaction } from "@/lib/transaction/importTransaction"; 26 | 27 | type CreateTransactionProps = { 28 | rpcUrl: string | null; 29 | multisigPda: string; 30 | vaultIndex: number; 31 | programId?: string; 32 | }; 33 | 34 | const CreateTransaction = ({ 35 | rpcUrl, 36 | multisigPda, 37 | vaultIndex, 38 | programId, 39 | }: CreateTransactionProps) => { 40 | const wallet = useWallet(); 41 | 42 | const [tx, setTx] = useState(""); 43 | const [open, setOpen] = useState(false); 44 | 45 | const connection = new Connection(rpcUrl || clusterApiUrl("mainnet-beta"), { 46 | commitment: "confirmed", 47 | }); 48 | 49 | const getSampleMessage = async () => { 50 | let memo = "Hello from Solana land!"; 51 | const vaultAddress = multisig.getVaultPda({ 52 | index: vaultIndex, 53 | multisigPda: new PublicKey(multisigPda), 54 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 55 | })[0]; 56 | 57 | const dummyMessage = Message.compile({ 58 | instructions: [ 59 | new TransactionInstruction({ 60 | keys: [ 61 | { 62 | pubkey: wallet.publicKey as PublicKey, 63 | isSigner: true, 64 | isWritable: true, 65 | }, 66 | ], 67 | data: Buffer.from(memo, "utf-8"), 68 | programId: new PublicKey( 69 | "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" 70 | ), 71 | }), 72 | ], 73 | payerKey: vaultAddress, 74 | recentBlockhash: (await connection.getLatestBlockhash()).blockhash, 75 | }); 76 | 77 | const encoded = bs58.default.encode(dummyMessage.serialize()); 78 | 79 | setTx(encoded); 80 | }; 81 | 82 | return ( 83 | 84 | 85 | Import Transaction 86 | 87 | 88 | 89 | Import Transaction 90 | 91 | Propose a transaction from a base58 encoded transaction message (not 92 | a transaction). 93 | 94 | 95 | setTx(e.target.value)} 100 | /> 101 |
102 | 122 | 147 |
148 | 154 |
155 |
156 | ); 157 | }; 158 | 159 | export default CreateTransaction; 160 | -------------------------------------------------------------------------------- /components/ExecuteButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { 3 | AddressLookupTableAccount, 4 | ComputeBudgetProgram, 5 | Connection, 6 | PublicKey, 7 | TransactionInstruction, 8 | TransactionMessage, 9 | VersionedTransaction, 10 | clusterApiUrl, 11 | } from '@solana/web3.js'; 12 | import { Button } from './ui/button'; 13 | import * as multisig from '@sqds/multisig'; 14 | import { useWallet } from '@solana/wallet-adapter-react'; 15 | import { useWalletModal } from '@solana/wallet-adapter-react-ui'; 16 | import { toast } from 'sonner'; 17 | import { useRouter } from 'next/navigation'; 18 | import { Dialog, DialogDescription, DialogHeader } from './ui/dialog'; 19 | import { DialogTrigger } from './ui/dialog'; 20 | import { DialogContent, DialogTitle } from './ui/dialog'; 21 | import { useState } from 'react'; 22 | import { Input } from './ui/input'; 23 | import { range } from '@/lib/utils'; 24 | 25 | type WithALT = { 26 | instruction: TransactionInstruction; 27 | lookupTableAccounts: AddressLookupTableAccount[]; 28 | }; 29 | 30 | type ExecuteButtonProps = { 31 | rpcUrl: string; 32 | multisigPda: string; 33 | transactionIndex: number; 34 | proposalStatus: string; 35 | programId: string; 36 | }; 37 | 38 | const ExecuteButton = ({ 39 | rpcUrl, 40 | multisigPda, 41 | transactionIndex, 42 | proposalStatus, 43 | programId, 44 | }: ExecuteButtonProps) => { 45 | const wallet = useWallet(); 46 | const walletModal = useWalletModal(); 47 | const router = useRouter(); 48 | const [priorityFeeLamports, setPriorityFeeLamports] = useState(5000); 49 | const [computeUnitBudget, setComputeUnitBudget] = useState(200_000); 50 | 51 | const isTransactionReady = proposalStatus === 'Approved'; 52 | const connection = new Connection(rpcUrl || clusterApiUrl('mainnet-beta'), { 53 | commitment: 'confirmed', 54 | }); 55 | 56 | const executeTransaction = async () => { 57 | if (!wallet.publicKey) { 58 | walletModal.setVisible(true); 59 | return; 60 | } 61 | const member = wallet.publicKey; 62 | if (!wallet.signAllTransactions) return; 63 | let bigIntTransactionIndex = BigInt(transactionIndex); 64 | 65 | if (!isTransactionReady) { 66 | toast.error('Proposal has not reached threshold.'); 67 | return; 68 | } 69 | 70 | console.log({ 71 | multisigPda: multisigPda, 72 | connection, 73 | member: member.toBase58(), 74 | transactionIndex: bigIntTransactionIndex, 75 | programId: programId ? programId : multisig.PROGRAM_ID.toBase58(), 76 | }); 77 | 78 | const [transactionPda] = multisig.getTransactionPda({ 79 | multisigPda: new PublicKey(multisigPda), 80 | index: bigIntTransactionIndex, 81 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 82 | }); 83 | 84 | let txData; 85 | let txType; 86 | try { 87 | await multisig.accounts.VaultTransaction.fromAccountAddress(connection, transactionPda); 88 | txType = 'vault'; 89 | } catch (error) { 90 | try { 91 | await multisig.accounts.ConfigTransaction.fromAccountAddress(connection, transactionPda); 92 | txType = 'config'; 93 | } catch (e) { 94 | txData = await multisig.accounts.Batch.fromAccountAddress(connection, transactionPda); 95 | txType = 'batch'; 96 | } 97 | } 98 | 99 | let transactions: VersionedTransaction[] = []; 100 | 101 | const priorityFeeInstruction = ComputeBudgetProgram.setComputeUnitPrice({ 102 | microLamports: priorityFeeLamports, 103 | }); 104 | const computeUnitInstruction = ComputeBudgetProgram.setComputeUnitLimit({ 105 | units: computeUnitBudget, 106 | }); 107 | 108 | let blockhash = (await connection.getLatestBlockhash()).blockhash; 109 | 110 | if (txType == 'vault') { 111 | const resp = await multisig.instructions.vaultTransactionExecute({ 112 | multisigPda: new PublicKey(multisigPda), 113 | connection, 114 | member, 115 | transactionIndex: bigIntTransactionIndex, 116 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 117 | }); 118 | transactions.push( 119 | new VersionedTransaction( 120 | new TransactionMessage({ 121 | instructions: [priorityFeeInstruction, computeUnitInstruction, resp.instruction], 122 | payerKey: member, 123 | recentBlockhash: blockhash, 124 | }).compileToV0Message(resp.lookupTableAccounts) 125 | ) 126 | ); 127 | } else if (txType == 'config') { 128 | const executeIx = multisig.instructions.configTransactionExecute({ 129 | multisigPda: new PublicKey(multisigPda), 130 | member, 131 | rentPayer: member, 132 | transactionIndex: bigIntTransactionIndex, 133 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 134 | }); 135 | transactions.push( 136 | new VersionedTransaction( 137 | new TransactionMessage({ 138 | instructions: [priorityFeeInstruction, computeUnitInstruction, executeIx], 139 | payerKey: member, 140 | recentBlockhash: blockhash, 141 | }).compileToV0Message() 142 | ) 143 | ); 144 | } else if (txType == 'batch' && txData) { 145 | const executedBatchIndex = txData.executedTransactionIndex; 146 | const batchSize = txData.size; 147 | 148 | if (executedBatchIndex === undefined || batchSize === undefined) { 149 | throw new Error( 150 | "executedBatchIndex or batchSize is undefined and can't execute the transaction" 151 | ); 152 | } 153 | 154 | transactions.push( 155 | ...(await Promise.all( 156 | range(executedBatchIndex + 1, batchSize).map(async (batchIndex) => { 157 | const { instruction: transactionExecuteIx, lookupTableAccounts } = 158 | await multisig.instructions.batchExecuteTransaction({ 159 | connection, 160 | member, 161 | batchIndex: bigIntTransactionIndex, 162 | transactionIndex: batchIndex, 163 | multisigPda: new PublicKey(multisigPda), 164 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 165 | }); 166 | 167 | const message = new TransactionMessage({ 168 | payerKey: member, 169 | recentBlockhash: blockhash, 170 | instructions: [priorityFeeInstruction, computeUnitInstruction, transactionExecuteIx], 171 | }).compileToV0Message(lookupTableAccounts); 172 | 173 | return new VersionedTransaction(message); 174 | }) 175 | )) 176 | ); 177 | } 178 | 179 | const signedTransactions = await wallet.signAllTransactions(transactions); 180 | 181 | for (const signedTx of signedTransactions) { 182 | const signature = await connection.sendRawTransaction(signedTx.serialize(), { 183 | skipPreflight: true, 184 | }); 185 | console.log('Transaction signature', signature); 186 | toast.loading('Confirming...', { 187 | id: 'transaction', 188 | }); 189 | await connection.getSignatureStatuses([signature]); 190 | await new Promise((resolve) => setTimeout(resolve, 1000)); 191 | } 192 | router.refresh(); 193 | }; 194 | return ( 195 | 196 | 200 | Execute 201 | 202 | 203 | 204 | Execute Transaction 205 | 206 | Select custom priority fees and compute unit limits and execute transaction. 207 | 208 | 209 |

Priority Fee in lamports

210 | setPriorityFeeLamports(Number(e.target.value))} 213 | value={priorityFeeLamports} 214 | /> 215 | 216 |

Compute Unit Budget

217 | setComputeUnitBudget(Number(e.target.value))} 220 | value={computeUnitBudget} 221 | /> 222 | 236 |
237 |
238 | ); 239 | }; 240 | 241 | export default ExecuteButton; 242 | -------------------------------------------------------------------------------- /components/MultisigInput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from "next/navigation"; 3 | import { useState } from "react"; 4 | import { Input } from "./ui/input"; 5 | import { Button } from "./ui/button"; 6 | 7 | const MultisigInput = () => { 8 | const [multisig, setMultisig] = useState(""); 9 | const router = useRouter(); 10 | 11 | const setMultisigCookie = () => { 12 | document.cookie = `x-multisig=${multisig}; path=/`; 13 | router.refresh(); 14 | }; 15 | 16 | return ( 17 |
18 |
19 |

Enter Multisig Address

20 |

21 | There is no multisig set for in Local Storage, set it by entering its 22 | Public Key in the Form below. 23 |

24 | setMultisig(e.target.value)} 29 | /> 30 | 33 |
34 |
35 | ); 36 | }; 37 | 38 | export default MultisigInput; 39 | -------------------------------------------------------------------------------- /components/RejectButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | Connection, 4 | PublicKey, 5 | Transaction, 6 | clusterApiUrl, 7 | } from "@solana/web3.js"; 8 | import { Button } from "./ui/button"; 9 | import * as multisig from "@sqds/multisig"; 10 | import { useWallet } from "@solana/wallet-adapter-react"; 11 | import { useWalletModal } from "@solana/wallet-adapter-react-ui"; 12 | import { toast } from "sonner"; 13 | import { useRouter } from "next/navigation"; 14 | 15 | type RejectButtonProps = { 16 | rpcUrl: string; 17 | multisigPda: string; 18 | transactionIndex: number; 19 | proposalStatus: string; 20 | programId: string; 21 | }; 22 | 23 | const RejectButton = ({ 24 | rpcUrl, 25 | multisigPda, 26 | transactionIndex, 27 | proposalStatus, 28 | programId, 29 | }: RejectButtonProps) => { 30 | const wallet = useWallet(); 31 | const walletModal = useWalletModal(); 32 | const router = useRouter(); 33 | 34 | const connection = new Connection(rpcUrl || clusterApiUrl("mainnet-beta"), { 35 | commitment: "confirmed", 36 | }); 37 | 38 | const validKinds = ["None", "Active", "Draft"]; 39 | const isKindValid = validKinds.includes(proposalStatus); 40 | 41 | const rejectTransaction = async () => { 42 | if (!wallet.publicKey) { 43 | walletModal.setVisible(true); 44 | return; 45 | } 46 | let bigIntTransactionIndex = BigInt(transactionIndex); 47 | 48 | if (!isKindValid) { 49 | toast.error("You can't reject this proposal."); 50 | return; 51 | } 52 | 53 | const transaction = new Transaction(); 54 | if (proposalStatus === "None") { 55 | const createProposalInstruction = multisig.instructions.proposalCreate({ 56 | multisigPda: new PublicKey(multisigPda), 57 | creator: wallet.publicKey, 58 | isDraft: false, 59 | transactionIndex: bigIntTransactionIndex, 60 | rentPayer: wallet.publicKey, 61 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 62 | }); 63 | transaction.add(createProposalInstruction); 64 | } 65 | if (proposalStatus == "Draft") { 66 | const activateProposalInstruction = 67 | multisig.instructions.proposalActivate({ 68 | multisigPda: new PublicKey(multisigPda), 69 | member: wallet.publicKey, 70 | transactionIndex: bigIntTransactionIndex, 71 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 72 | }); 73 | transaction.add(activateProposalInstruction); 74 | } 75 | const rejectProposalInstruction = multisig.instructions.proposalReject({ 76 | multisigPda: new PublicKey(multisigPda), 77 | member: wallet.publicKey, 78 | transactionIndex: bigIntTransactionIndex, 79 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 80 | }); 81 | 82 | transaction.add(rejectProposalInstruction); 83 | 84 | const signature = await wallet.sendTransaction(transaction, connection, { 85 | skipPreflight: true, 86 | }); 87 | console.log("Transaction signature", signature); 88 | toast.loading("Confirming...", { 89 | id: "transaction", 90 | }); 91 | await connection.getSignatureStatuses([signature]); 92 | await new Promise((resolve) => setTimeout(resolve, 1000)); 93 | router.refresh(); 94 | }; 95 | return ( 96 | 110 | ); 111 | }; 112 | 113 | export default RejectButton; 114 | -------------------------------------------------------------------------------- /components/RemoveMemberButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | Connection, 4 | PublicKey, 5 | TransactionMessage, 6 | VersionedTransaction, 7 | } from "@solana/web3.js"; 8 | import { Button } from "./ui/button"; 9 | import * as multisig from "@sqds/multisig"; 10 | import { useWallet } from "@solana/wallet-adapter-react"; 11 | import { useWalletModal } from "@solana/wallet-adapter-react-ui"; 12 | import { toast } from "sonner"; 13 | import { useRouter } from "next/navigation"; 14 | 15 | type RemoveMemberButtonProps = { 16 | rpcUrl: string; 17 | multisigPda: string; 18 | transactionIndex: number; 19 | memberKey: string; 20 | programId: string; 21 | }; 22 | 23 | const RemoveMemberButton = ({ 24 | rpcUrl, 25 | multisigPda, 26 | transactionIndex, 27 | memberKey, 28 | programId, 29 | }: RemoveMemberButtonProps) => { 30 | const wallet = useWallet(); 31 | const walletModal = useWalletModal(); 32 | const router = useRouter(); 33 | 34 | const member = new PublicKey(memberKey); 35 | 36 | const connection = new Connection(rpcUrl, { commitment: "confirmed" }); 37 | 38 | const removeMember = async () => { 39 | if (!wallet.publicKey) { 40 | walletModal.setVisible(true); 41 | return; 42 | } 43 | let bigIntTransactionIndex = BigInt(transactionIndex); 44 | 45 | const removeMemberIx = multisig.instructions.configTransactionCreate({ 46 | multisigPda: new PublicKey(multisigPda), 47 | actions: [ 48 | { 49 | __kind: "RemoveMember", 50 | oldMember: member, 51 | }, 52 | ], 53 | creator: wallet.publicKey, 54 | transactionIndex: bigIntTransactionIndex, 55 | rentPayer: wallet.publicKey, 56 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 57 | }); 58 | const proposalIx = multisig.instructions.proposalCreate({ 59 | multisigPda: new PublicKey(multisigPda), 60 | creator: wallet.publicKey, 61 | isDraft: false, 62 | transactionIndex: bigIntTransactionIndex, 63 | rentPayer: wallet.publicKey, 64 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 65 | }); 66 | const approveIx = multisig.instructions.proposalApprove({ 67 | multisigPda: new PublicKey(multisigPda), 68 | member: wallet.publicKey, 69 | transactionIndex: bigIntTransactionIndex, 70 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 71 | }); 72 | 73 | const message = new TransactionMessage({ 74 | instructions: [removeMemberIx, proposalIx, approveIx], 75 | payerKey: wallet.publicKey, 76 | recentBlockhash: (await connection.getLatestBlockhash()).blockhash, 77 | }).compileToV0Message(); 78 | 79 | const transaction = new VersionedTransaction(message); 80 | 81 | const signature = await wallet.sendTransaction(transaction, connection, { 82 | skipPreflight: true, 83 | }); 84 | console.log("Transaction signature", signature); 85 | toast.loading("Confirming...", { 86 | id: "transaction", 87 | }); 88 | await connection.getSignatureStatuses([signature]); 89 | await new Promise((resolve) => setTimeout(resolve, 1000)); 90 | router.refresh(); 91 | }; 92 | return ( 93 | 106 | ); 107 | }; 108 | 109 | export default RemoveMemberButton; 110 | -------------------------------------------------------------------------------- /components/RenderMultisigRoute.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { usePathname } from "next/navigation"; 3 | import MultisigInput from "./MultisigInput"; 4 | 5 | interface RenderRouteProps { 6 | multisig: boolean; 7 | children: React.ReactNode; 8 | } 9 | 10 | export default function RenderMultisigRoute({ 11 | multisig, 12 | children, 13 | }: RenderRouteProps) { 14 | const pathname = usePathname(); 15 | 16 | return ( 17 |
18 | {multisig ? ( 19 |
{children}
20 | ) : ( 21 | <> 22 | {pathname == "/settings" || pathname == "/create" ? ( 23 |
{children}
24 | ) : ( 25 | 26 | )} 27 | 28 | )} 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/SendSolButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogHeader, 7 | DialogTitle, 8 | DialogTrigger, 9 | } from "@/components/ui/dialog"; 10 | import { Button } from "./ui/button"; 11 | import { useState } from "react"; 12 | import * as multisig from "@sqds/multisig"; 13 | import { useWallet } from "@solana/wallet-adapter-react"; 14 | import { 15 | Connection, 16 | LAMPORTS_PER_SOL, 17 | PublicKey, 18 | SystemProgram, 19 | TransactionMessage, 20 | VersionedTransaction, 21 | clusterApiUrl, 22 | } from "@solana/web3.js"; 23 | import { useWalletModal } from "@solana/wallet-adapter-react-ui"; 24 | import { Input } from "./ui/input"; 25 | import { toast } from "sonner"; 26 | import { useRouter } from "next/navigation"; 27 | import { isPublickey } from "@/lib/isPublickey"; 28 | 29 | type SendSolProps = { 30 | rpcUrl: string; 31 | multisigPda: string; 32 | vaultIndex: number; 33 | programId?: string; 34 | }; 35 | 36 | const SendSol = ({ 37 | rpcUrl, 38 | multisigPda, 39 | vaultIndex, 40 | programId, 41 | }: SendSolProps) => { 42 | const wallet = useWallet(); 43 | const walletModal = useWalletModal(); 44 | const [amount, setAmount] = useState(0); 45 | const [recipient, setRecipient] = useState(""); 46 | const router = useRouter(); 47 | 48 | const transfer = async () => { 49 | if (!wallet.publicKey) { 50 | return; 51 | } 52 | 53 | const vaultAddress = multisig.getVaultPda({ 54 | index: vaultIndex, 55 | multisigPda: new PublicKey(multisigPda), 56 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 57 | })[0]; 58 | 59 | const transferInstruction = SystemProgram.transfer({ 60 | fromPubkey: vaultAddress, 61 | toPubkey: new PublicKey(recipient), 62 | lamports: amount * LAMPORTS_PER_SOL, 63 | }); 64 | 65 | const connection = new Connection(rpcUrl || clusterApiUrl("mainnet-beta"), { 66 | commitment: "confirmed", 67 | }); 68 | 69 | const multisigInfo = await multisig.accounts.Multisig.fromAccountAddress( 70 | connection, 71 | new PublicKey(multisigPda) 72 | ); 73 | 74 | const blockhash = (await connection.getLatestBlockhash()).blockhash; 75 | 76 | const transferMessage = new TransactionMessage({ 77 | instructions: [transferInstruction], 78 | payerKey: wallet.publicKey, 79 | recentBlockhash: blockhash, 80 | }); 81 | 82 | const transactionIndex = Number(multisigInfo.transactionIndex) + 1; 83 | const transactionIndexBN = BigInt(transactionIndex); 84 | 85 | const multisigTransactionIx = multisig.instructions.vaultTransactionCreate({ 86 | multisigPda: new PublicKey(multisigPda), 87 | creator: wallet.publicKey, 88 | ephemeralSigners: 0, 89 | transactionMessage: transferMessage, 90 | transactionIndex: transactionIndexBN, 91 | addressLookupTableAccounts: [], 92 | rentPayer: wallet.publicKey, 93 | vaultIndex: vaultIndex, 94 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 95 | }); 96 | const proposalIx = multisig.instructions.proposalCreate({ 97 | multisigPda: new PublicKey(multisigPda), 98 | creator: wallet.publicKey, 99 | isDraft: false, 100 | transactionIndex: transactionIndexBN, 101 | rentPayer: wallet.publicKey, 102 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 103 | }); 104 | const approveIx = multisig.instructions.proposalApprove({ 105 | multisigPda: new PublicKey(multisigPda), 106 | member: wallet.publicKey, 107 | transactionIndex: transactionIndexBN, 108 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 109 | }); 110 | 111 | const message = new TransactionMessage({ 112 | instructions: [multisigTransactionIx, proposalIx, approveIx], 113 | payerKey: wallet.publicKey, 114 | recentBlockhash: blockhash, 115 | }).compileToV0Message(); 116 | 117 | const transaction = new VersionedTransaction(message); 118 | 119 | const signature = await wallet.sendTransaction(transaction, connection, { 120 | skipPreflight: true, 121 | }); 122 | console.log("Transaction signature", signature); 123 | toast.loading("Confirming...", { 124 | id: "transaction", 125 | }); 126 | await connection.getSignatureStatuses([signature]); 127 | await new Promise((resolve) => setTimeout(resolve, 1000)); 128 | router.refresh(); 129 | }; 130 | 131 | return ( 132 | 133 | 134 | 141 | 142 | 143 | 144 | Transfer SOL 145 | 146 | Create a proposal to transfer SOL to another address. 147 | 148 | 149 | setRecipient(e.target.value)} 153 | /> 154 | {isPublickey(recipient) ? null : ( 155 |

Invalid recipient address

156 | )} 157 | setAmount(parseInt(e.target.value))} 161 | /> 162 | 175 |
176 |
177 | ); 178 | }; 179 | 180 | export default SendSol; 181 | -------------------------------------------------------------------------------- /components/SendTokensButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogHeader, 7 | DialogTitle, 8 | DialogTrigger, 9 | } from "@/components/ui/dialog"; 10 | import { Button } from "./ui/button"; 11 | import { useState } from "react"; 12 | import { 13 | createAssociatedTokenAccountIdempotentInstruction, 14 | createTransferCheckedInstruction, 15 | getAssociatedTokenAddressSync, 16 | } from "@solana/spl-token"; 17 | import * as multisig from "@sqds/multisig"; 18 | import { useWallet } from "@solana/wallet-adapter-react"; 19 | import { 20 | Connection, 21 | PublicKey, 22 | TransactionMessage, 23 | VersionedTransaction, 24 | clusterApiUrl, 25 | } from "@solana/web3.js"; 26 | import { useWalletModal } from "@solana/wallet-adapter-react-ui"; 27 | import { Input } from "./ui/input"; 28 | import { toast } from "sonner"; 29 | import { useRouter } from "next/navigation"; 30 | import { isPublickey } from "@/lib/isPublickey"; 31 | 32 | type SendTokensProps = { 33 | tokenAccount: string; 34 | mint: string; 35 | decimals: number; 36 | rpcUrl: string; 37 | multisigPda: string; 38 | vaultIndex: number; 39 | programId?: string; 40 | }; 41 | 42 | const SendTokens = ({ 43 | tokenAccount, 44 | mint, 45 | decimals, 46 | rpcUrl, 47 | multisigPda, 48 | vaultIndex, 49 | programId, 50 | }: SendTokensProps) => { 51 | const wallet = useWallet(); 52 | const walletModal = useWalletModal(); 53 | const [amount, setAmount] = useState(0); 54 | const [recipient, setRecipient] = useState(""); 55 | const router = useRouter(); 56 | 57 | const transfer = async () => { 58 | if (!wallet.publicKey) { 59 | return; 60 | } 61 | const recipientATA = getAssociatedTokenAddressSync( 62 | new PublicKey(mint), 63 | new PublicKey(recipient), 64 | true 65 | ); 66 | 67 | const vaultAddress = multisig 68 | .getVaultPda({ 69 | index: vaultIndex, 70 | multisigPda: new PublicKey(multisigPda), 71 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 72 | })[0] 73 | .toBase58(); 74 | 75 | const createRecipientATAInstruction = 76 | createAssociatedTokenAccountIdempotentInstruction( 77 | new PublicKey(vaultAddress), 78 | recipientATA, 79 | new PublicKey(recipient), 80 | new PublicKey(mint) 81 | ); 82 | 83 | const transferInstruction = createTransferCheckedInstruction( 84 | new PublicKey(tokenAccount), 85 | new PublicKey(mint), 86 | recipientATA, 87 | new PublicKey(vaultAddress), 88 | amount * 10 ** decimals, 89 | decimals 90 | ); 91 | 92 | const connection = new Connection(rpcUrl || clusterApiUrl("mainnet-beta"), { 93 | commitment: "confirmed", 94 | }); 95 | 96 | const multisigInfo = await multisig.accounts.Multisig.fromAccountAddress( 97 | connection, 98 | new PublicKey(multisigPda) 99 | ); 100 | 101 | const blockhash = (await connection.getLatestBlockhash()).blockhash; 102 | 103 | const transferMessage = new TransactionMessage({ 104 | instructions: [createRecipientATAInstruction, transferInstruction], 105 | payerKey: wallet.publicKey, 106 | recentBlockhash: blockhash, 107 | }); 108 | 109 | const transactionIndex = Number(multisigInfo.transactionIndex) + 1; 110 | const transactionIndexBN = BigInt(transactionIndex); 111 | 112 | const multisigTransactionIx = multisig.instructions.vaultTransactionCreate({ 113 | multisigPda: new PublicKey(multisigPda), 114 | creator: wallet.publicKey, 115 | ephemeralSigners: 0, 116 | transactionMessage: transferMessage, 117 | transactionIndex: transactionIndexBN, 118 | addressLookupTableAccounts: [], 119 | rentPayer: wallet.publicKey, 120 | vaultIndex: vaultIndex, 121 | }); 122 | const proposalIx = multisig.instructions.proposalCreate({ 123 | multisigPda: new PublicKey(multisigPda), 124 | creator: wallet.publicKey, 125 | isDraft: false, 126 | transactionIndex: transactionIndexBN, 127 | rentPayer: wallet.publicKey, 128 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 129 | }); 130 | const approveIx = multisig.instructions.proposalApprove({ 131 | multisigPda: new PublicKey(multisigPda), 132 | member: wallet.publicKey, 133 | transactionIndex: transactionIndexBN, 134 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 135 | }); 136 | 137 | const message = new TransactionMessage({ 138 | instructions: [multisigTransactionIx, proposalIx, approveIx], 139 | payerKey: wallet.publicKey, 140 | recentBlockhash: blockhash, 141 | }).compileToV0Message(); 142 | 143 | const transaction = new VersionedTransaction(message); 144 | 145 | const signature = await wallet.sendTransaction(transaction, connection, { 146 | skipPreflight: true, 147 | }); 148 | console.log("Transaction signature", signature); 149 | toast.loading("Confirming...", { 150 | id: "transaction", 151 | }); 152 | await connection.getSignatureStatuses([signature]); 153 | await new Promise((resolve) => setTimeout(resolve, 1000)); 154 | router.refresh(); 155 | }; 156 | 157 | return ( 158 | 159 | 160 | 169 | 170 | 171 | 172 | Transfer tokens 173 | 174 | Create a proposal to transfer tokens to another address. 175 | 176 | 177 | setRecipient(e.target.value)} 181 | /> 182 | {isPublickey(recipient) ? null : ( 183 |

Invalid recipient address

184 | )} 185 | setAmount(parseInt(e.target.value))} 189 | /> 190 | 203 |
204 |
205 | ); 206 | }; 207 | 208 | export default SendTokens; 209 | -------------------------------------------------------------------------------- /components/SetProgramIdInput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState } from "react"; 3 | import { Input } from "./ui/input"; 4 | import { Button } from "./ui/button"; 5 | import { useRouter } from "next/navigation"; 6 | import { toast } from "sonner"; 7 | import { isPublickey } from "@/lib/isPublickey"; 8 | 9 | const SetProgramIdInput = () => { 10 | const [programId, setProgramId] = useState(""); 11 | const router = useRouter(); 12 | 13 | const publicKeyTest = isPublickey(programId); 14 | 15 | const onSubmit = async () => { 16 | // Needs to use an RPC that isn't the public endpoint 17 | // const programTest = await isProgram(programId); 18 | if (publicKeyTest) { 19 | document.cookie = `x-program-id=${programId}`; 20 | setProgramId(""); 21 | router.refresh(); 22 | } else { 23 | throw "Please enter a valid program."; 24 | } 25 | }; 26 | 27 | return ( 28 |
29 | setProgramId(e.target.value)} 31 | placeholder="SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf" 32 | defaultValue={programId} 33 | className="" 34 | /> 35 | {!publicKeyTest && programId.length > 0 && ( 36 |

Please enter a valid key.

37 | )} 38 | 51 |
52 | ); 53 | }; 54 | 55 | export default SetProgramIdInput; 56 | -------------------------------------------------------------------------------- /components/SetRpcUrlnput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState } from "react"; 3 | import { Input } from "./ui/input"; 4 | import { Button } from "./ui/button"; 5 | import { useRouter } from "next/navigation"; 6 | import { toast } from "sonner"; 7 | 8 | const SetRpcUrlInput = () => { 9 | const [rpcUrl, setRpcUrl] = useState(""); 10 | const router = useRouter(); 11 | const isValidUrl = (url: string) => { 12 | const urlPattern = new RegExp( 13 | "^(https?:\\/\\/)?" + // validate protocol 14 | "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name 15 | "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address 16 | "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path 17 | "(\\?[;&a-z\\d%_.~+=-]*)?" + // query string 18 | "(\\#[-a-z\\d_]*)?$", 19 | "i" 20 | ); 21 | return !!urlPattern.test(url); 22 | }; 23 | 24 | const onSubmit = async () => { 25 | if (isValidUrl(rpcUrl)) { 26 | document.cookie = `x-rpc-url=${rpcUrl}`; 27 | setRpcUrl(""); 28 | router.refresh(); 29 | } else { 30 | throw "Please enter a valid URL."; 31 | } 32 | }; 33 | 34 | return ( 35 |
36 | setRpcUrl(e.target.value)} 38 | placeholder="https://api.mainnet-beta.solana.com" 39 | defaultValue={rpcUrl} 40 | className="" 41 | /> 42 | {!isValidUrl(rpcUrl) && rpcUrl.length > 0 && ( 43 |

Please enter a valid URL.

44 | )} 45 | 59 |
60 | ); 61 | }; 62 | 63 | export default SetRpcUrlInput; 64 | -------------------------------------------------------------------------------- /components/TokenList.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AccountInfo, 3 | LAMPORTS_PER_SOL, 4 | ParsedAccountData, 5 | PublicKey, 6 | RpcResponseAndContext, 7 | } from "@solana/web3.js"; 8 | import { 9 | Card, 10 | CardContent, 11 | CardDescription, 12 | CardHeader, 13 | CardTitle, 14 | } from "./ui/card"; 15 | import SendTokens from "./SendTokensButton"; 16 | import SendSol from "./SendSolButton"; 17 | 18 | type TokenListProps = { 19 | solBalance: number; 20 | tokens: RpcResponseAndContext< 21 | { 22 | pubkey: PublicKey; 23 | account: AccountInfo; 24 | }[] 25 | >; 26 | rpcUrl: string; 27 | multisigPda: string; 28 | vaultIndex: number; 29 | programId?: string; 30 | }; 31 | 32 | export function TokenList({ 33 | solBalance, 34 | tokens, 35 | rpcUrl, 36 | multisigPda, 37 | vaultIndex, 38 | programId, 39 | }: TokenListProps) { 40 | return ( 41 | 42 | 43 | Tokens 44 | The tokens you hold in your wallet 45 | 46 | 47 |
48 |
49 |
50 |
51 |

SOL

52 |

53 | Amount: {solBalance / LAMPORTS_PER_SOL} 54 |

55 |
56 |
57 | 63 |
64 |
65 | {tokens.value.length > 0 ?
: null} 66 |
67 | {tokens.value.map((token) => ( 68 |
69 |
70 |
71 |

72 | Mint: {token.account.data.parsed.info.mint} 73 |

74 |

75 | Amount:{" "} 76 | {token.account.data.parsed.info.tokenAmount.uiAmount} 77 |

78 |
79 |
80 | 91 |
92 |
93 |
94 |
95 | ))} 96 |
97 |
98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /components/TransactionTable.tsx: -------------------------------------------------------------------------------- 1 | import * as multisig from "@sqds/multisig"; 2 | import ApproveButton from "./ApproveButton"; 3 | import ExecuteButton from "./ExecuteButton"; 4 | import RejectButton from "./RejectButton"; 5 | import { TableBody, TableCell, TableRow } from "./ui/table"; 6 | import Link from "next/link"; 7 | 8 | interface ActionButtonsProps { 9 | rpcUrl: string; 10 | multisigPda: string; 11 | transactionIndex: number; 12 | proposalStatus: string; 13 | programId: string; 14 | } 15 | 16 | export default function TransactionTable({ 17 | multisigPda, 18 | rpcUrl, 19 | transactions, 20 | programId, 21 | }: { 22 | multisigPda: string; 23 | rpcUrl: string; 24 | transactions: { 25 | transactionPda: string; 26 | proposal: multisig.generated.Proposal | null; 27 | index: bigint; 28 | }[]; 29 | programId?: string; 30 | }) { 31 | if (transactions.length === 0) { 32 | 33 | 34 | No transactions found. 35 | 36 | ; 37 | } 38 | return ( 39 | 40 | {transactions.map((transaction, index) => { 41 | return ( 42 | 43 | {Number(transaction.index)} 44 | 45 | 51 | {transaction.transactionPda} 52 | 53 | 54 | 55 | {transaction.proposal?.status.__kind || "None"} 56 | 57 | 58 | 67 | 68 | 69 | ); 70 | })} 71 | 72 | ); 73 | } 74 | 75 | function ActionButtons({ 76 | rpcUrl, 77 | multisigPda, 78 | transactionIndex, 79 | proposalStatus, 80 | programId, 81 | }: ActionButtonsProps) { 82 | return ( 83 | <> 84 | 91 | 98 | 105 | 106 | ); 107 | } 108 | 109 | function createSolanaExplorerUrl(publicKey: string, rpcUrl: string): string { 110 | const baseUrl = "https://explorer.solana.com/address/"; 111 | const clusterQuery = "?cluster=custom&customUrl="; 112 | const encodedRpcUrl = encodeURIComponent(rpcUrl); 113 | 114 | return `${baseUrl}${publicKey}${clusterQuery}${encodedRpcUrl}`; 115 | } 116 | -------------------------------------------------------------------------------- /components/VaultDisplayer.tsx: -------------------------------------------------------------------------------- 1 | import * as multisig from "@sqds/multisig"; 2 | import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; 3 | import { PublicKey } from "@solana/web3.js"; 4 | import { VaultSelector } from "./VaultSelector"; 5 | 6 | type VaultDisplayerProps = { 7 | multisigPdaString: string; 8 | vaultIndex: number; 9 | programId?: string; 10 | }; 11 | 12 | export function VaultDisplayer({ 13 | multisigPdaString, 14 | vaultIndex, 15 | programId, 16 | }: VaultDisplayerProps) { 17 | const vaultAddress = multisig.getVaultPda({ 18 | multisigPda: new PublicKey(multisigPdaString), 19 | index: vaultIndex, 20 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID, 21 | }); 22 | 23 | return ( 24 | 25 | 26 | Squads Vault 27 | 28 | 29 |

Address: {vaultAddress[0].toBase58()}

30 |
31 | 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /components/VaultSelector.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import * as React from "react"; 3 | import { Check, ChevronsUpDown } from "lucide-react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | import { Button } from "@/components/ui/button"; 7 | import { 8 | Command, 9 | CommandEmpty, 10 | CommandGroup, 11 | CommandInput, 12 | CommandItem, 13 | } from "@/components/ui/command"; 14 | import { 15 | Popover, 16 | PopoverContent, 17 | PopoverTrigger, 18 | } from "@/components/ui/popover"; 19 | import { useRouter } from "next/navigation"; 20 | 21 | // Generate vault indices from 0 to 255 22 | const vaultIndices = Array.from({ length: 16 }, (_, index) => ({ 23 | value: index.toString(), 24 | label: `Vault ${index}`, 25 | })); 26 | 27 | export function VaultSelector() { 28 | const [open, setOpen] = React.useState(false); 29 | const [value, setValue] = React.useState(""); 30 | 31 | const router = useRouter(); 32 | 33 | React.useEffect(() => { 34 | document.cookie = `x-vault-index=${value}; path=/`; 35 | router.refresh(); 36 | }, [value]); 37 | 38 | return ( 39 | 40 | 41 | 50 | 51 | 52 | 53 | 54 | No vault index found. 55 | 56 | {vaultIndices.map((vaultIndex) => ( 57 | { 61 | setValue(currentValue === value ? "" : currentValue); 62 | setOpen(false); 63 | }} 64 | > 65 | 71 | {vaultIndex.label} 72 | 73 | ))} 74 | 75 | 76 | 77 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /components/Wallet.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { FC, useMemo } from "react"; 3 | import { 4 | ConnectionProvider, 5 | WalletProvider, 6 | } from "@solana/wallet-adapter-react"; 7 | import { WalletAdapterNetwork } from "@solana/wallet-adapter-base"; 8 | import { WalletModalProvider } from "@solana/wallet-adapter-react-ui"; 9 | import { clusterApiUrl } from "@solana/web3.js"; 10 | 11 | require("@solana/wallet-adapter-react-ui/styles.css"); 12 | 13 | type Props = { 14 | children?: React.ReactNode; 15 | }; 16 | 17 | export const Wallet: FC = ({ children }) => { 18 | const network = WalletAdapterNetwork.Devnet; 19 | 20 | const endpoint = useMemo(() => clusterApiUrl(network), [network]); 21 | 22 | const wallets = useMemo( 23 | () => [], 24 | 25 | [network] 26 | ); 27 | 28 | return ( 29 | 30 | 31 | {children} 32 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /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", 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 | -------------------------------------------------------------------------------- /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 { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /components/ui/command.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { type DialogProps } from "@radix-ui/react-dialog" 5 | import { Command as CommandPrimitive } from "cmdk" 6 | import { Search } from "lucide-react" 7 | 8 | import { cn } from "@/lib/utils" 9 | import { Dialog, DialogContent } from "@/components/ui/dialog" 10 | 11 | const Command = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 23 | )) 24 | Command.displayName = CommandPrimitive.displayName 25 | 26 | interface CommandDialogProps extends DialogProps {} 27 | 28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => { 29 | return ( 30 | 31 | 32 | 33 | {children} 34 | 35 | 36 | 37 | ) 38 | } 39 | 40 | const CommandInput = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 |
45 | 46 | 54 |
55 | )) 56 | 57 | CommandInput.displayName = CommandPrimitive.Input.displayName 58 | 59 | const CommandList = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, ...props }, ref) => ( 63 | 68 | )) 69 | 70 | CommandList.displayName = CommandPrimitive.List.displayName 71 | 72 | const CommandEmpty = React.forwardRef< 73 | React.ElementRef, 74 | React.ComponentPropsWithoutRef 75 | >((props, ref) => ( 76 | 81 | )) 82 | 83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName 84 | 85 | const CommandGroup = React.forwardRef< 86 | React.ElementRef, 87 | React.ComponentPropsWithoutRef 88 | >(({ className, ...props }, ref) => ( 89 | 97 | )) 98 | 99 | CommandGroup.displayName = CommandPrimitive.Group.displayName 100 | 101 | const CommandSeparator = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )) 111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName 112 | 113 | const CommandItem = React.forwardRef< 114 | React.ElementRef, 115 | React.ComponentPropsWithoutRef 116 | >(({ className, ...props }, ref) => ( 117 | 125 | )) 126 | 127 | CommandItem.displayName = CommandPrimitive.Item.displayName 128 | 129 | const CommandShortcut = ({ 130 | className, 131 | ...props 132 | }: React.HTMLAttributes) => { 133 | return ( 134 | 141 | ) 142 | } 143 | CommandShortcut.displayName = "CommandShortcut" 144 | 145 | export { 146 | Command, 147 | CommandDialog, 148 | CommandInput, 149 | CommandList, 150 | CommandEmpty, 151 | CommandGroup, 152 | CommandItem, 153 | CommandShortcut, 154 | CommandSeparator, 155 | } 156 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /components/ui/pagination.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | import { ButtonProps, buttonVariants } from "@/components/ui/button"; 6 | import Link from "next/link"; 7 | 8 | const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( 9 |