Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 32 additions & 22 deletions src/components/SendTokensButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,44 @@ 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<string>('');
const [recipient, setRecipient] = useState('');
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);
Expand All @@ -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(
Expand All @@ -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({
Expand All @@ -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();
Expand All @@ -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('');
Expand Down Expand Up @@ -132,9 +142,9 @@ const SendTokens = ({tokenAccount, mint, decimals, multisigPda}: SendTokensProps
Create a proposal to transfer tokens to another address.
</DialogDescription>
</DialogHeader>
<Input placeholder="Recipient" type="text" onChange={(e) => setRecipient(e.target.value)}/>
<Input placeholder="Recipient" type="text" onChange={(e) => setRecipient(e.target.value)} />
{isPublickey(recipient) ? null : <p className="text-xs">Invalid recipient address</p>}
<Input placeholder="Amount" type="number" onChange={(e) => setAmount(e.target.value)}/>
<Input placeholder="Amount" type="number" onChange={(e) => setAmount(e.target.value)} />
{!isAmountValid && amount.length > 0 && (
<p className="text-xs text-red-500">Invalid amount</p>
)}
Expand Down
54 changes: 32 additions & 22 deletions src/components/TokenList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -34,29 +35,38 @@ export function TokenList({ multisigPda }: TokenListProps) {
{tokens && tokens.value.length > 0 ? <hr className="mt-2" /> : null}
</div>
{tokens &&
tokens.value.map((token) => (
<div key={token.account.data.parsed.info.mint}>
<div className="flex items-center">
<div className="ml-4 space-y-1">
<p className="text-sm font-medium leading-none">
Mint: {token.account.data.parsed.info.mint}
</p>
<p className="text-sm text-muted-foreground">
Amount: {token.account.data.parsed.info.tokenAmount.uiAmount}
</p>
tokens.value
.filter(token => token.account.data.parsed.info.tokenAmount.uiAmount > 0)
.map((token) => {
const isToken2022 = token.account.owner.toBase58() === 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
return (
<div key={token.account.data.parsed.info.mint}>
<div className="flex items-center">
<div className="ml-4 space-y-1">
<p className="text-sm font-medium leading-none">
Mint: {token.account.data.parsed.info.mint}
<span className="ml-2 text-xs text-muted-foreground">
({isToken2022 ? 'SPL-Token-2022' : 'SPL Token'})
</span>
</p>
<p className="text-sm text-muted-foreground">
Amount: {token.account.data.parsed.info.tokenAmount.uiAmount}
</p>
</div>
<div className="ml-auto">
<SendTokens
mint={token.account.data.parsed.info.mint}
tokenAccount={token.pubkey.toBase58()}
decimals={token.account.data.parsed.info.tokenAmount.decimals}
multisigPda={multisigPda}
isToken2022={isToken2022}
/>
</div>
</div>
<hr className="mt-2" />
</div>
<div className="ml-auto">
<SendTokens
mint={token.account.data.parsed.info.mint}
tokenAccount={token.pubkey.toBase58()}
decimals={token.account.data.parsed.info.tokenAmount.decimals}
multisigPda={multisigPda}
/>
</div>
</div>
<hr className="mt-2" />
</div>
))}
);
})}
</div>
</CardContent>
</Card>
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useMultisigData.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
44 changes: 28 additions & 16 deletions src/hooks/useServices.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -14,17 +14,18 @@ export interface TransactionObject {

// load multisig
export const useMultisig = () => {
const {rpcUrl, programId, multisigAddress} = useMultisigData();
const { rpcUrl, programId, multisigAddress } = useMultisigData();
const wallet = useWallet();

return useSuspenseQuery({
queryKey: ['multisig', multisigAddress],
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) {
Expand All @@ -36,7 +37,7 @@ export const useMultisig = () => {
};

export const useBalance = () => {
const {connection, multisigVault} = useMultisigData();
const { connection, multisigVault } = useMultisigData();

return useSuspenseQuery({
queryKey: ['balance', multisigVault?.toBase58()],
Expand All @@ -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;
Expand All @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
}
},
"include": [
"**/*.ts"
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules",
Expand Down