diff --git a/src/components/SendTokensButton.tsx b/src/components/SendTokensButton.tsx index 9b24cf4..b34e544 100644 --- a/src/components/SendTokensButton.tsx +++ b/src/components/SendTokensButton.tsx @@ -6,33 +6,36 @@ import { DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; -import {Button} from './ui/button'; -import {useState} from 'react'; +import { Button } from './ui/button'; +import { useState } from 'react'; import { createAssociatedTokenAccountIdempotentInstruction, createTransferCheckedInstruction, getAssociatedTokenAddressSync, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, } from '@solana/spl-token'; -import {useWallet} from '@solana/wallet-adapter-react'; -import {PublicKey, TransactionMessage, VersionedTransaction} from '@solana/web3.js'; -import {useWalletModal} from '@solana/wallet-adapter-react-ui'; -import {Input} from './ui/input'; -import {toast} from 'sonner'; -import {isPublickey} from '@/lib/isPublickey'; -import {useMultisigData} from '@/hooks/useMultisigData'; -import {useQueryClient} from '@tanstack/react-query'; -import {createSquadTransactionInstructions} from '@/lib/createSquadTransactionInstructions'; -import {useAccess} from "../lib/hooks/useAccess"; -import {waitForConfirmation} from "../lib/transactionConfirmation"; +import { useWallet } from '@solana/wallet-adapter-react'; +import { PublicKey, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; +import { useWalletModal } from '@solana/wallet-adapter-react-ui'; +import { Input } from './ui/input'; +import { toast } from 'sonner'; +import { isPublickey } from '@/lib/isPublickey'; +import { useMultisigData } from '@/hooks/useMultisigData'; +import { useQueryClient } from '@tanstack/react-query'; +import { createSquadTransactionInstructions } from '@/lib/createSquadTransactionInstructions'; +import { useAccess } from "../lib/hooks/useAccess"; +import { waitForConfirmation } from "../lib/transactionConfirmation"; type SendTokensProps = { tokenAccount: string; mint: string; decimals: number; multisigPda: string; + isToken2022?: boolean; }; -const SendTokens = ({tokenAccount, mint, decimals, multisigPda}: SendTokensProps) => { +const SendTokens = ({ tokenAccount, mint, decimals, multisigPda, isToken2022 = false }: SendTokensProps) => { const wallet = useWallet(); const walletModal = useWalletModal(); const [amount, setAmount] = useState(''); @@ -40,7 +43,7 @@ const SendTokens = ({tokenAccount, mint, decimals, multisigPda}: SendTokensProps const access = useAccess(); const [isOpen, setIsOpen] = useState(false); const closeDialog = () => setIsOpen(false); - const {connection, multisigVault, rpcUrl, programId} = useMultisigData(); + const { connection, multisigVault, rpcUrl, programId } = useMultisigData(); const queryClient = useQueryClient(); const parsedAmount = parseFloat(amount); @@ -50,17 +53,22 @@ const SendTokens = ({tokenAccount, mint, decimals, multisigPda}: SendTokensProps if (!wallet.publicKey || !multisigVault) { return; } + + const tokenProgramId = isToken2022 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID; + const recipientATA = getAssociatedTokenAddressSync( new PublicKey(mint), new PublicKey(recipient), - true + true, + tokenProgramId ); const createRecipientATAInstruction = createAssociatedTokenAccountIdempotentInstruction( new PublicKey(multisigVault), recipientATA, new PublicKey(recipient), - new PublicKey(mint) + new PublicKey(mint), + tokenProgramId ); const transferInstruction = createTransferCheckedInstruction( @@ -69,7 +77,9 @@ const SendTokens = ({tokenAccount, mint, decimals, multisigPda}: SendTokensProps recipientATA, new PublicKey(multisigVault), parsedAmount * 10 ** decimals, - decimals + decimals, + undefined, + tokenProgramId ); const instructions = await createSquadTransactionInstructions({ @@ -83,7 +93,7 @@ const SendTokens = ({tokenAccount, mint, decimals, multisigPda}: SendTokensProps const blockhash = (await connection.getLatestBlockhash()).blockhash; const message = new TransactionMessage({ - instructions: instructions, + instructions, payerKey: wallet.publicKey, recentBlockhash: blockhash, }).compileToV0Message(); @@ -102,7 +112,7 @@ const SendTokens = ({tokenAccount, mint, decimals, multisigPda}: SendTokensProps if (!sent.every((sent) => !!sent)) { throw `Unable to confirm transaction`; } - await queryClient.invalidateQueries({queryKey: ['transactions']}); + await queryClient.invalidateQueries({ queryKey: ['transactions'] }); await new Promise((resolve) => setTimeout(resolve, 500)); setAmount(''); setRecipient(''); @@ -132,9 +142,9 @@ const SendTokens = ({tokenAccount, mint, decimals, multisigPda}: SendTokensProps Create a proposal to transfer tokens to another address. - setRecipient(e.target.value)}/> + setRecipient(e.target.value)} /> {isPublickey(recipient) ? null :

Invalid recipient address

} - setAmount(e.target.value)}/> + setAmount(e.target.value)} /> {!isAmountValid && amount.length > 0 && (

Invalid amount

)} diff --git a/src/components/TokenList.tsx b/src/components/TokenList.tsx index 5a17580..eb9763f 100644 --- a/src/components/TokenList.tsx +++ b/src/components/TokenList.tsx @@ -3,6 +3,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/ import SendTokens from './SendTokensButton'; import SendSol from './SendSolButton'; import { useBalance, useGetTokens } from '@/hooks/useServices'; +import React from 'react'; type TokenListProps = { multisigPda: string; @@ -34,29 +35,38 @@ export function TokenList({ multisigPda }: TokenListProps) { {tokens && tokens.value.length > 0 ?
: null} {tokens && - tokens.value.map((token) => ( -
-
-
-

- Mint: {token.account.data.parsed.info.mint} -

-

- Amount: {token.account.data.parsed.info.tokenAmount.uiAmount} -

+ tokens.value + .filter(token => token.account.data.parsed.info.tokenAmount.uiAmount > 0) + .map((token) => { + const isToken2022 = token.account.owner.toBase58() === 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'; + return ( +
+
+
+

+ Mint: {token.account.data.parsed.info.mint} + + ({isToken2022 ? 'SPL-Token-2022' : 'SPL Token'}) + +

+

+ Amount: {token.account.data.parsed.info.tokenAmount.uiAmount} +

+
+
+ +
+
+
-
- -
-
-
-
- ))} + ); + })}
diff --git a/src/hooks/useMultisigData.tsx b/src/hooks/useMultisigData.tsx index 6f367bb..fd82095 100644 --- a/src/hooks/useMultisigData.tsx +++ b/src/hooks/useMultisigData.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { clusterApiUrl, Connection, PublicKey } from '@solana/web3.js'; -import { useRpcUrl, useProgramId } from '@/hooks/useSettings'; -import { useMultisigAddress } from '@/hooks/useMultisigAddress'; +import { useRpcUrl, useProgramId } from './useSettings'; +import { useMultisigAddress } from './useMultisigAddress'; import { DEFAULT_MULTISIG_PROGRAM_ID, getAuthorityPDA } from '@sqds/sdk'; import BN from 'bn.js'; diff --git a/src/hooks/useServices.tsx b/src/hooks/useServices.tsx index c733a53..e29585f 100644 --- a/src/hooks/useServices.tsx +++ b/src/hooks/useServices.tsx @@ -1,10 +1,10 @@ 'use client'; -import {useSuspenseQuery} from '@tanstack/react-query'; -import {PublicKey} from '@solana/web3.js'; -import {useMultisigData} from '@/hooks/useMultisigData'; -import Squads, {getTxPDA, TransactionAccount} from '@sqds/sdk'; -import {useWallet} from '@solana/wallet-adapter-react'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { PublicKey } from '@solana/web3.js'; +import { useMultisigData } from './useMultisigData'; +import Squads, { getTxPDA, TransactionAccount } from '@sqds/sdk'; +import { useWallet } from '@solana/wallet-adapter-react'; import BN from 'bn.js'; export interface TransactionObject { @@ -14,7 +14,7 @@ export interface TransactionObject { // load multisig export const useMultisig = () => { - const {rpcUrl, programId, multisigAddress} = useMultisigData(); + const { rpcUrl, programId, multisigAddress } = useMultisigData(); const wallet = useWallet(); return useSuspenseQuery({ @@ -22,9 +22,10 @@ export const useMultisig = () => { queryFn: async () => { if (!multisigAddress) return null; try { + const multisigPubkey = new PublicKey(multisigAddress); - const squads = Squads.endpoint(rpcUrl, wallet as any, {multisigProgramId: programId}); + const squads = Squads.endpoint(rpcUrl, wallet as any, { multisigProgramId: programId }); return squads.getMultisig(multisigPubkey); } catch (error) { @@ -36,7 +37,7 @@ export const useMultisig = () => { }; export const useBalance = () => { - const {connection, multisigVault} = useMultisigData(); + const { connection, multisigVault } = useMultisigData(); return useSuspenseQuery({ queryKey: ['balance', multisigVault?.toBase58()], @@ -53,16 +54,27 @@ export const useBalance = () => { }; export const useGetTokens = () => { - const {connection, multisigVault} = useMultisigData(); + const { connection, multisigVault } = useMultisigData(); return useSuspenseQuery({ queryKey: ['tokenBalances', multisigVault?.toBase58()], queryFn: async () => { if (!multisigVault) return null; try { - return connection.getParsedTokenAccountsByOwner(multisigVault, { - programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), - }); + // Fetch both Token-2022 and regular SPL tokens + const [token2022Accounts, splAccounts] = await Promise.all([ + connection.getParsedTokenAccountsByOwner(multisigVault, { + programId: new PublicKey('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'), + }), + connection.getParsedTokenAccountsByOwner(multisigVault, { + programId: new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), + }), + ]); + + // Combine both token lists + return { + value: [...token2022Accounts.value, ...splAccounts.value], + }; } catch (error) { console.error(error); return null; @@ -87,22 +99,22 @@ async function fetchTransactionData( return null; } - return {account: transaction, address: transactionPda}; + return { account: transaction, address: transactionPda }; } export const useTransactions = (startIndex: number, endIndex: number) => { - const {programId, multisigAddress, rpcUrl} = useMultisigData(); + const { programId, multisigAddress, rpcUrl } = useMultisigData(); const wallet = useWallet(); return useSuspenseQuery({ - queryKey: ['transactions', {startIndex, endIndex, multisigAddress, programId: programId.toBase58()}], + queryKey: ['transactions', { startIndex, endIndex, multisigAddress, programId: programId.toBase58() }], queryFn: async () => { if (!multisigAddress) return null; try { const multisigPda = new PublicKey(multisigAddress); const results: TransactionObject[] = []; - const squads = Squads.endpoint(rpcUrl, wallet as any, {multisigProgramId: programId}); + const squads = Squads.endpoint(rpcUrl, wallet as any, { multisigProgramId: programId }); for (let i = 0; i <= startIndex - endIndex; i++) { const index = BigInt(startIndex - i); diff --git a/tsconfig.json b/tsconfig.json index 9013ceb..4d7017f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,7 +25,8 @@ } }, "include": [ - "**/*.ts" + "**/*.ts", + "**/*.tsx" ], "exclude": [ "node_modules",