├── .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 |
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 |
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 |
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 |
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 |
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 |
15 | );
16 |
17 | const PaginationContent = React.forwardRef<
18 | HTMLUListElement,
19 | React.ComponentProps<"ul">
20 | >(({ className, ...props }, ref) => (
21 |
26 | ));
27 | PaginationContent.displayName = "PaginationContent";
28 |
29 | const PaginationItem = React.forwardRef<
30 | HTMLLIElement,
31 | React.ComponentProps<"li">
32 | >(({ className, ...props }, ref) => (
33 |
34 | ));
35 | PaginationItem.displayName = "PaginationItem";
36 |
37 | type PaginationLinkProps = {
38 | isActive?: boolean;
39 | } & Pick &
40 | React.ComponentProps;
41 |
42 | const PaginationLink = ({
43 | className,
44 | isActive,
45 | size = "icon",
46 | ...props
47 | }: PaginationLinkProps) => (
48 |
49 |
60 |
61 | );
62 | PaginationLink.displayName = "PaginationLink";
63 |
64 | const PaginationPrevious = ({
65 | className,
66 | ...props
67 | }: React.ComponentProps) => (
68 |
74 |
75 | Previous
76 |
77 | );
78 | PaginationPrevious.displayName = "PaginationPrevious";
79 |
80 | const PaginationNext = ({
81 | className,
82 | ...props
83 | }: React.ComponentProps) => (
84 |
90 | Next
91 |
92 |
93 | );
94 |
95 | const PaginationEllipsis = ({
96 | className,
97 | ...props
98 | }: React.ComponentProps<"span">) => (
99 |
104 |
105 | More pages
106 |
107 | );
108 |
109 | export {
110 | Pagination,
111 | PaginationContent,
112 | PaginationEllipsis,
113 | PaginationItem,
114 | PaginationLink,
115 | PaginationNext,
116 | PaginationPrevious,
117 | };
118 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ))
135 | SelectItem.displayName = SelectPrimitive.Item.displayName
136 |
137 | const SelectSeparator = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef
140 | >(({ className, ...props }, ref) => (
141 |
146 | ))
147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
148 |
149 | export {
150 | Select,
151 | SelectGroup,
152 | SelectValue,
153 | SelectTrigger,
154 | SelectContent,
155 | SelectLabel,
156 | SelectItem,
157 | SelectSeparator,
158 | SelectScrollUpButton,
159 | SelectScrollDownButton,
160 | }
161 |
--------------------------------------------------------------------------------
/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ))
17 | Table.displayName = "Table"
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ))
25 | TableHeader.displayName = "TableHeader"
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ))
37 | TableBody.displayName = "TableBody"
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:last:border-b-0",
47 | className
48 | )}
49 | {...props}
50 | />
51 | ))
52 | TableFooter.displayName = "TableFooter"
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ))
67 | TableRow.displayName = "TableRow"
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 | |
81 | ))
82 | TableHead.displayName = "TableHead"
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 | |
93 | ))
94 | TableCell.displayName = "TableCell"
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ))
106 | TableCaption.displayName = "TableCaption"
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | }
118 |
--------------------------------------------------------------------------------
/lib/createSquad.ts:
--------------------------------------------------------------------------------
1 | import * as web3 from "@solana/web3.js";
2 | import * as multisig from "@sqds/multisig";
3 | import { PublicKey } from "@solana/web3.js";
4 |
5 | export interface Member {
6 | key: web3.PublicKey | null;
7 | permissions: multisig.generated.Permissions;
8 | }
9 |
10 | export async function createMultisig(
11 | connection: web3.Connection,
12 | user: web3.PublicKey,
13 | members: Member[],
14 | threshold: number,
15 | createKey: web3.PublicKey,
16 | rentCollector?: string,
17 | configAuthority?: string,
18 | programId?: string
19 | ) {
20 | try {
21 | const multisigPda = multisig.getMultisigPda({
22 | createKey,
23 | programId: programId
24 | ? new web3.PublicKey(programId)
25 | : multisig.PROGRAM_ID,
26 | })[0];
27 |
28 | const [programConfig] = multisig.getProgramConfigPda({
29 | programId: programId
30 | ? new web3.PublicKey(programId)
31 | : multisig.PROGRAM_ID,
32 | });
33 |
34 | const programConfigInfo =
35 | await multisig.accounts.ProgramConfig.fromAccountAddress(
36 | connection,
37 | programConfig
38 | );
39 |
40 | const configTreasury = programConfigInfo.treasury;
41 |
42 | const ix = multisig.instructions.multisigCreateV2({
43 | multisigPda: multisigPda,
44 | createKey: createKey,
45 | creator: user,
46 | members: members as any,
47 | threshold: threshold,
48 | configAuthority: configAuthority ? new PublicKey(configAuthority) : null,
49 | treasury: configTreasury,
50 | rentCollector: rentCollector ? new PublicKey(rentCollector) : null,
51 | timeLock: 0,
52 | programId: programId
53 | ? new web3.PublicKey(programId)
54 | : multisig.PROGRAM_ID,
55 | });
56 |
57 | const tx = new web3.Transaction().add(ix);
58 |
59 | tx.feePayer = user;
60 | tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
61 |
62 | return { transaction: tx, multisig: multisigPda };
63 | } catch (err) {
64 | throw err;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/lib/hooks/useSquadForm.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useCallback, useState } from "react";
3 |
4 | type SubmitHandler = () => Promise;
5 |
6 | export interface FormValues {
7 | [key: string]: any;
8 | }
9 |
10 | export type ValidationErrors = Record;
11 | export type ValidationFunction = (value: any) => Promise;
12 | export interface ValidationRules {
13 | [key: string]: ValidationFunction;
14 | }
15 |
16 | export interface FormState {
17 | values: FormValues;
18 | errors: ValidationErrors;
19 | isValid: boolean;
20 | isLoading: boolean;
21 | }
22 |
23 | export function useSquadForm(
24 | initialValues: FormValues,
25 | validationRules: ValidationRules
26 | ) {
27 | const [formState, setFormState] = useState({
28 | values: initialValues,
29 | errors: {},
30 | isValid: true,
31 | isLoading: false,
32 | });
33 |
34 | const validateField = useCallback(
35 | async (field: string, value: any) => {
36 | if (validationRules[field]) {
37 | const error = await validationRules[field](value);
38 | return error;
39 | }
40 | return null;
41 | },
42 | [validationRules]
43 | );
44 |
45 | const handleChange = useCallback(
46 | async (field: string, value: any) => {
47 | setFormState((prev) => ({
48 | ...prev,
49 | isLoading: true,
50 | values: { ...prev.values, [field]: value },
51 | }));
52 |
53 | const error = await validateField(field, value);
54 |
55 | setFormState((prev) => {
56 | const newErrors = { ...prev.errors, [field]: error || "" };
57 | const isValid = Object.values(newErrors).every((err) => !err);
58 |
59 | return {
60 | ...prev,
61 | errors: newErrors,
62 | isValid,
63 | isLoading: false,
64 | };
65 | });
66 | },
67 | [validateField]
68 | );
69 |
70 | const handleAddMember = (e: any) => {
71 | e.preventDefault();
72 | handleChange("members", {
73 | count: formState.values.members.count + 1,
74 | memberData: [
75 | ...formState.values.members.memberData,
76 | {
77 | key: null,
78 | permissions: {
79 | mask: 0,
80 | },
81 | },
82 | ],
83 | });
84 | };
85 |
86 | const onSubmit = async (handler: SubmitHandler): Promise => {
87 | setFormState((prev) => ({
88 | ...prev,
89 | isLoading: true,
90 | }));
91 | try {
92 | return await handler();
93 | } catch (error: any) {
94 | console.error(error);
95 | throw error;
96 | } finally {
97 | setFormState((prev) => ({
98 | ...prev,
99 | isLoading: false,
100 | }));
101 | }
102 | };
103 |
104 | return { formState, handleChange, handleAddMember, onSubmit };
105 | }
106 |
--------------------------------------------------------------------------------
/lib/isProgram.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Connection, PublicKey, clusterApiUrl } from "@solana/web3.js";
3 |
4 | export async function isProgram(key: string, rpcUrl?: string) {
5 | const connection = new Connection(rpcUrl || clusterApiUrl("mainnet-beta"), {
6 | commitment: "confirmed",
7 | });
8 | try {
9 | const pk = new PublicKey(key);
10 | const info = await connection.getAccountInfo(pk);
11 | if (info == null) {
12 | return false;
13 | } else {
14 | if (info.executable) {
15 | return true;
16 | }
17 |
18 | return false;
19 | }
20 | } catch (err) {
21 | return false;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/isPublickey.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { PublicKey } from "@solana/web3.js";
3 |
4 | export function isPublickey(key: string) {
5 | try {
6 | const pk = new PublicKey(key);
7 | if (pk) {
8 | return true;
9 | } else {
10 | console.log("Invalid public key");
11 | return false;
12 | }
13 | } catch (err) {
14 | return false;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/lib/transaction/decodeAndDeserialize.ts:
--------------------------------------------------------------------------------
1 | import * as bs58 from "bs58";
2 | import {
3 | Message,
4 | MessageAccountKeys,
5 | MessageV0,
6 | PublicKey,
7 | Transaction,
8 | TransactionMessage,
9 | VersionedMessage,
10 | VersionedTransaction,
11 | } from "@solana/web3.js";
12 |
13 | interface DeserializedTransaction {
14 | message: TransactionMessage;
15 | version: number | "legacy";
16 | accountKeys: PublicKey[];
17 | }
18 |
19 | /**
20 | * Decodes a base58 encoded transaction and deserializes it into a TransactionMessage
21 | * @param tx - Base58 encoded transaction string
22 | * @returns Object containing the deserialized message, version, and account keys
23 | * @throws Error if deserialization fails
24 | */
25 | export function decodeAndDeserialize(tx: string): DeserializedTransaction {
26 | if (!tx) {
27 | throw new Error("Transaction string is required");
28 | }
29 |
30 | try {
31 | const messageBytes = bs58.default.decode(tx);
32 | const version = VersionedMessage.deserializeMessageVersion(messageBytes);
33 | let message: TransactionMessage;
34 | let accountKeys: PublicKey[];
35 |
36 | if (version === "legacy") {
37 | const legacyMessage = Message.from(messageBytes);
38 | accountKeys = legacyMessage.accountKeys;
39 |
40 | const intermediate = VersionedMessage.deserialize(
41 | new MessageV0(legacyMessage).serialize()
42 | );
43 | message = TransactionMessage.decompile(intermediate, {
44 | addressLookupTableAccounts: [],
45 | });
46 | } else {
47 | const versionedMessage = VersionedMessage.deserialize(messageBytes);
48 | accountKeys = versionedMessage.staticAccountKeys;
49 |
50 | message = TransactionMessage.decompile(versionedMessage, {
51 | addressLookupTableAccounts: [],
52 | });
53 | }
54 |
55 | return {
56 | version,
57 | message,
58 | accountKeys,
59 | };
60 | } catch (error) {
61 | if (error instanceof Error) {
62 | throw new Error(`Failed to decode transaction: ${error.message}`);
63 | }
64 | throw new Error("Failed to decode transaction: Unknown error");
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/lib/transaction/getAccountsForSimulation.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AddressLookupTableAccount,
3 | Connection,
4 | Message,
5 | SystemProgram,
6 | VersionedMessage,
7 | VersionedTransaction,
8 | } from "@solana/web3.js";
9 |
10 | export async function getAccountsForSimulation(
11 | connection: Connection,
12 | tx: VersionedTransaction,
13 | isLegacy: boolean
14 | ): Promise {
15 | if (isLegacy) {
16 | return (tx.message as Message)
17 | .nonProgramIds()
18 | .map((pubkey) => pubkey.toString())
19 | .filter((address) => address !== SystemProgram.programId.toBase58());
20 | } else {
21 | const addressLookupTableAccounts = await loadLookupTables(
22 | connection,
23 | tx.message
24 | );
25 |
26 | const { staticAccountKeys, accountKeysFromLookups } =
27 | tx.message.getAccountKeys({ addressLookupTableAccounts });
28 |
29 | const staticAddresses = staticAccountKeys.reduce((acc, k) => {
30 | if (!k.equals(SystemProgram.programId)) {
31 | acc.push(k.toString());
32 | }
33 | return acc;
34 | }, [] as string[]);
35 |
36 | const addressesFromLookups = accountKeysFromLookups
37 | ? accountKeysFromLookups.writable.map((k) => k.toString())
38 | : [];
39 |
40 | return [...new Set([...staticAddresses, ...addressesFromLookups])];
41 | }
42 | }
43 |
44 | export async function loadLookupTables(
45 | connection: Connection,
46 | transactionMessage: VersionedMessage
47 | ) {
48 | const addressLookupTableAccounts: AddressLookupTableAccount[] = [];
49 | const { addressTableLookups } = transactionMessage;
50 | if (addressTableLookups.length > 0) {
51 | for (const addressTableLookup of addressTableLookups) {
52 | const { value } = await connection.getAddressLookupTable(
53 | addressTableLookup.accountKey
54 | );
55 | if (!value) continue;
56 |
57 | addressLookupTableAccounts.push(value);
58 | }
59 | }
60 | return addressLookupTableAccounts;
61 | }
62 |
--------------------------------------------------------------------------------
/lib/transaction/importTransaction.ts:
--------------------------------------------------------------------------------
1 | import * as multisig from "@sqds/multisig";
2 | import {
3 | Connection,
4 | PublicKey,
5 | TransactionMessage,
6 | VersionedMessage,
7 | VersionedTransaction,
8 | } from "@solana/web3.js";
9 | import { decodeAndDeserialize } from "./decodeAndDeserialize";
10 | import { WalletContextState } from "@solana/wallet-adapter-react";
11 | import { toast } from "sonner";
12 | import { loadLookupTables } from "./getAccountsForSimulation";
13 |
14 | export const importTransaction = async (
15 | tx: string,
16 | connection: Connection,
17 | multisigPda: string,
18 | programId: string,
19 | vaultIndex: number,
20 | wallet: WalletContextState
21 | ) => {
22 | if (!wallet.publicKey) {
23 | throw "Please connect your wallet.";
24 | }
25 | try {
26 | const { message, version } = decodeAndDeserialize(tx);
27 |
28 | const multisigInfo = await multisig.accounts.Multisig.fromAccountAddress(
29 | connection,
30 | new PublicKey(multisigPda)
31 | );
32 |
33 | const transactionMessage = new TransactionMessage(message);
34 |
35 | const addressLookupTableAccounts =
36 | version === 0
37 | ? await loadLookupTables(
38 | connection,
39 | transactionMessage.compileToV0Message()
40 | )
41 | : [];
42 |
43 | const transactionIndex = Number(multisigInfo.transactionIndex) + 1;
44 | const transactionIndexBN = BigInt(transactionIndex);
45 |
46 | const multisigTransactionIx = multisig.instructions.vaultTransactionCreate({
47 | multisigPda: new PublicKey(multisigPda),
48 | creator: wallet.publicKey,
49 | ephemeralSigners: 0,
50 | transactionMessage: transactionMessage,
51 | transactionIndex: transactionIndexBN,
52 | addressLookupTableAccounts,
53 | rentPayer: wallet.publicKey,
54 | vaultIndex: vaultIndex,
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: transactionIndexBN,
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: transactionIndexBN,
69 | programId: programId ? new PublicKey(programId) : multisig.PROGRAM_ID,
70 | });
71 |
72 | const blockhash = (await connection.getLatestBlockhash()).blockhash;
73 |
74 | const wrappedMessage = new TransactionMessage({
75 | instructions: [multisigTransactionIx, proposalIx, approveIx],
76 | payerKey: wallet.publicKey,
77 | recentBlockhash: blockhash,
78 | }).compileToV0Message();
79 |
80 | const transaction = new VersionedTransaction(wrappedMessage);
81 |
82 | const signature = await wallet.sendTransaction(transaction, connection, {
83 | skipPreflight: true,
84 | });
85 | console.log("Transaction signature", signature);
86 | toast.loading("Confirming...", {
87 | id: "transaction",
88 | });
89 |
90 | let sent = false;
91 | const maxAttempts = 10;
92 | const delayMs = 1000;
93 | for (let attempt = 0; attempt <= maxAttempts && !sent; attempt++) {
94 | const status = await connection.getSignatureStatus(signature);
95 | if (status?.value?.confirmationStatus === "confirmed") {
96 | await new Promise((resolve) => setTimeout(resolve, delayMs));
97 | sent = true;
98 | } else {
99 | await new Promise((resolve) => setTimeout(resolve, delayMs));
100 | }
101 | }
102 | } catch (error) {
103 | console.error(error);
104 | }
105 | };
106 |
--------------------------------------------------------------------------------
/lib/transaction/simulateEncodedTransaction.ts:
--------------------------------------------------------------------------------
1 | import { toast } from "sonner";
2 | import { decodeAndDeserialize } from "./decodeAndDeserialize";
3 | import { Connection, VersionedTransaction } from "@solana/web3.js";
4 | import { WalletContextState } from "@solana/wallet-adapter-react";
5 | import { getAccountsForSimulation } from "./getAccountsForSimulation";
6 |
7 | export const simulateEncodedTransaction = async (
8 | tx: string,
9 | connection: Connection,
10 | wallet: WalletContextState
11 | ) => {
12 | if (!wallet.publicKey) {
13 | throw "Please connect your wallet.";
14 | }
15 | try {
16 | const { message, version } = decodeAndDeserialize(tx);
17 |
18 | const transaction = new VersionedTransaction(message.compileToV0Message());
19 |
20 | const keys = await getAccountsForSimulation(
21 | connection,
22 | transaction,
23 | version === 0
24 | );
25 |
26 | toast.loading("Simulating...", {
27 | id: "simulation",
28 | });
29 | const { value } = await connection.simulateTransaction(transaction, {
30 | sigVerify: false,
31 | replaceRecentBlockhash: true,
32 | commitment: "confirmed",
33 | accounts: {
34 | encoding: "base64",
35 | addresses: keys,
36 | },
37 | });
38 |
39 | if (value.err) {
40 | console.error(value.err);
41 | throw "Simulation failed";
42 | }
43 | } catch (error: any) {
44 | console.error(error);
45 | throw new Error(error);
46 | }
47 | };
48 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
8 | export function range(start: number, end: number): number[] {
9 | const result: number[] = [];
10 | for (let i = start; i <= end; i++) {
11 | result.push(i);
12 | }
13 | return result;
14 | }
15 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 |
3 | export async function middleware(req: NextRequest) {
4 | // Create supabase client and get session
5 | const res = NextResponse.next();
6 |
7 | // Set the "x-pathname" header
8 | const requestHeaders = new Headers(req.headers);
9 | requestHeaders.set("x-pathname", req.nextUrl.pathname);
10 |
11 | const cookie = req.cookies.get("x-multisig")?.value;
12 | if (cookie) {
13 | requestHeaders.set("x-multisig", cookie);
14 | }
15 |
16 | const rpcUrl = req.cookies.get("x-rpc-url")?.value || process.env.NEXT_PUBLIC_RPC_URL;
17 | if (rpcUrl) {
18 | requestHeaders.set("x-rpc-url", rpcUrl);
19 | }
20 |
21 | const vaultIndex = req.cookies.get("x-vault-index")?.value;
22 | if (vaultIndex) {
23 | requestHeaders.set("x-vault-index", vaultIndex);
24 | }
25 |
26 | return NextResponse.next({
27 | request: {
28 | headers: requestHeaders,
29 | },
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | remotePatterns: [
5 | {
6 | protocol: "https",
7 | hostname: "drive.google.com",
8 | },
9 | ],
10 | },
11 | };
12 |
13 | module.exports = nextConfig;
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "squads-v4-public-ui",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@radix-ui/react-dialog": "^1.0.5",
13 | "@radix-ui/react-popover": "^1.0.7",
14 | "@radix-ui/react-select": "^2.0.0",
15 | "@radix-ui/react-slot": "^1.0.2",
16 | "@solana/spl-token": "^0.3.11",
17 | "@solana/wallet-adapter-base": "^0.9.23",
18 | "@solana/wallet-adapter-react": "^0.15.35",
19 | "@solana/wallet-adapter-react-ui": "^0.9.35",
20 | "@solana/web3.js": "^1.89.0",
21 | "@sqds/multisig": "^2.1.3",
22 | "bs58": "^6.0.0",
23 | "class-variance-authority": "^0.7.0",
24 | "clsx": "^2.1.0",
25 | "cmdk": "^0.2.0",
26 | "lucide-react": "^0.309.0",
27 | "next": "14.0.4",
28 | "next-themes": "^0.2.1",
29 | "react": "^18",
30 | "react-dom": "^18",
31 | "sonner": "^1.3.1",
32 | "tailwind-merge": "^2.2.0",
33 | "tailwindcss-animate": "^1.0.7"
34 | },
35 | "devDependencies": {
36 | "@types/node": "^20",
37 | "@types/react": "^18",
38 | "@types/react-dom": "^18",
39 | "autoprefixer": "^10.0.1",
40 | "eslint": "^8",
41 | "eslint-config-next": "14.0.4",
42 | "postcss": "^8",
43 | "prettier": "^3.5.1",
44 | "tailwindcss": "^3.3.0",
45 | "typescript": "^5"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" },
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" },
69 | },
70 | },
71 | animation: {
72 | "accordion-down": "accordion-down 0.2s ease-out",
73 | "accordion-up": "accordion-up 0.2s ease-out",
74 | },
75 | },
76 | },
77 | plugins: [require("tailwindcss-animate")],
78 | } satisfies Config
79 |
80 | export default config
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------