Skip to content

Solana transaction support #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"@solana/web3.js": "1.77.3",
"@reown/appkit-adapter-solana": "^1.6.3"
},
"dependencies": {
"@ai-sdk/anthropic": "^1.0.2",
Expand Down
19 changes: 17 additions & 2 deletions src/components/AccountContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import React, {
useState,
useEffect,
} from "react";
import { EVMWalletAdapter } from "../types";
import { EVMWalletAdapter, SolanaWallet } from "../types";

interface AccountContextType {
wallet: Wallet;
account: Account;
accountId: string | null;
evmWallet?: EVMWalletAdapter;
evmAddress?: string;
solanaWallet?: SolanaWallet;
solanaAddress?: string;
}

const AccountContext = createContext<AccountContextType | undefined>(undefined);
Expand All @@ -24,15 +26,18 @@ interface AccountProviderProps {
wallet: any;
account: any;
evmWallet?: EVMWalletAdapter;
solanaWallet?: SolanaWallet
}

//TODO fetch solana address
export function AccountProvider({
children,
wallet,
account,
evmWallet,
solanaWallet,
}: AccountProviderProps) {
const [accountId, setAccountId] = useState<string | null>(null);
const [solWallet, setSolWallet] = useState<SolanaWallet>();

useEffect(() => {
const getAccountId = async () => {
Expand All @@ -44,6 +49,15 @@ export function AccountProvider({
getAccountId();
}, [wallet, account, accountId]);

useEffect(() => {
const setupSolanaWallet = async () => {
if (solanaWallet?.provider) {
setSolWallet(solanaWallet);
}
};
setupSolanaWallet();
}, [solanaWallet]);

return (
<AccountContext.Provider
value={{
Expand All @@ -52,6 +66,7 @@ export function AccountProvider({
accountId,
evmWallet,
evmAddress: evmWallet?.address,
solanaWallet: solWallet,
}}
>
{children}
Expand Down
4 changes: 3 additions & 1 deletion src/components/BitteAiChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ export const BitteAiChat = ({
wallet,
apiUrl,
evmWallet,
solanaWallet,
}: BitteAiChatProps) => {
return (
<AccountProvider wallet={wallet} account={account} evmWallet={evmWallet}>
<AccountProvider wallet={wallet} account={account} evmWallet={evmWallet} solanaWallet={solanaWallet}>
<ChatContent
id={id}
creator={creator}
Expand All @@ -30,6 +31,7 @@ export const BitteAiChat = ({
account={account}
wallet={wallet}
apiUrl={apiUrl}
solanaWallet={solanaWallet}
/>
</AccountProvider>
);
Expand Down
4 changes: 3 additions & 1 deletion src/components/chat/ChatContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
BitteAiChatProps,
ChatRequestBody,
} from "../../types/types";
import { AccountProvider, useAccount } from "../AccountContext";
import { useAccount } from "../AccountContext";
import { Button } from "../ui/button";
import ShareModal from "../ui/modal/ShareModal";
import { BitteSpinner } from "./BitteSpinner";
Expand All @@ -43,6 +43,7 @@ export const ChatContent = ({
isShare,
colors = defaultColors,
apiUrl,
solanaWallet,
}: BitteAiChatProps) => {
const chatId = useRef(id || generateId()).current;
const [isAtBottom, setIsAtBottom] = useState(true);
Expand Down Expand Up @@ -91,6 +92,7 @@ export const ChatContent = ({
},
accountId: accountId || "",
evmAddress: evmAddress,
solanaWallet: solanaWallet,
} satisfies ChatRequestBody,
});

Expand Down
1 change: 1 addition & 0 deletions src/components/chat/EvmTxCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const EvmTxCard = ({ evmData }: { evmData?: SignRequestData }) => {
const [isLoading, setIsLoading] = useState(false);
const [txHash, setTxHash] = useState<string | undefined>();
const { evmAddress, evmWallet } = useAccount();
//TODO add estimated fees back in

if (!evmData)
return (
Expand Down
27 changes: 18 additions & 9 deletions src/components/chat/MessageGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { EvmTxCard } from "./EvmTxCard";
import { SAMessage } from "./Message";
import { ReviewTransaction } from "./ReviewTransaction";
import ShareDropButton from "./ShareDropButton";
import { SolTxCard } from "./SolTxCard";

interface MessageGroupProps {
groupKey: string;
Expand Down Expand Up @@ -109,19 +110,27 @@ export const MessageGroup = ({
if (
toolName === BittePrimitiveName.GENERATE_TRANSACTION ||
toolName === BittePrimitiveName.TRANSFER_FT ||
toolName === BittePrimitiveName.GENERATE_EVM_TX
toolName === BittePrimitiveName.GENERATE_EVM_TX ||
toolName === BittePrimitiveName.GENERATE_SOL_TX
) {
const [transactions, evmSignRequest] =
result.data && "evmSignRequest" in result.data
? [result.data.transactions, result.data.evmSignRequest]
: [result.data, undefined];

let transactions, evmSignRequest, solSignRequest;

if (result.data?.evmSignRequest) {
transactions = result.data.transactions;
evmSignRequest = result.data.evmSignRequest;
} else if (result.data?.solSignRequest) {
transactions = result.data.transactions;
solSignRequest = result.data.solSignRequest;
} else {
transactions = result.data;
}

return (
<ErrorBoundary key={`${groupKey}-${message.id}`}>
{evmSignRequest ? (
<EvmTxCard
evmData={evmSignRequest}
/>
<EvmTxCard evmData={evmSignRequest} />
) : solSignRequest ? (
<SolTxCard solData={solSignRequest} />
) : (
<ReviewTransaction
creator={creator}
Expand Down
204 changes: 204 additions & 0 deletions src/components/chat/SolTxCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
"use client";
import { useState, useEffect } from "react";
import { useWindowSize } from "../../hooks/useWindowSize";
import {
SystemProgram,
Transaction,
PublicKey,
LAMPORTS_PER_SOL,
Connection,
} from "@solana/web3.js";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "../ui/accordion";
import { Card, CardHeader, CardFooter } from "../ui/card";
import { CopyStandard } from "./CopyStandard";
import { TransactionDetail } from "./TransactionDetail";
import { Button } from "../ui/button";
import { useAccount } from "../AccountContext";
import LoadingMessage from "./LoadingMessage";
import { TransactionResult } from "./TransactionResult";
import { useTransaction } from "../../hooks/useTransaction";
import { SolSignRequest } from "../../types";

export const SolTxCard = ({ solData }: { solData?: SolSignRequest }) => {
const { width } = useWindowSize();
const isMobile = !!width && width < 640;
const [errorMsg, setErrorMsg] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [txResult, setTxResult] = useState<{signatures: string[], confirmations: any[]} | undefined>();
const { solanaWallet, solanaAddress } = useAccount();

if (!solanaWallet || !solData)
return (
<p className='my-6 overflow-auto text-center'>
Unable to create Solana transaction.
</p>
);

if (!Array.isArray(solData.params) || solData.params.length === 0) {
return (
<p className='my-6 overflow-auto text-center'>
Invalid Solana transaction parameters.
</p>
);
}

const { provider, connection } = solanaWallet;

const { handleTxn } = useTransaction({
solanaProvider: provider,
solanaConnection: connection,
});

const handleSmartAction = async () => {
setIsLoading(true);
try {
const result = await handleTxn({ solData: solData });
if (result.solana) {
setTxResult({
signatures: result.solana.signatures,
confirmations: result.solana.confirmations
});
}
} catch (error: any) {
setErrorMsg(error.message);
} finally {
setIsLoading(false);
}
};

return (
<>
<div className='mb-8 flex justify-center'>
<Card className='w-full'>
<CardHeader className='border-b border-slate-200 p-4 text-center md:p-6'>
<p className='text-xl font-semibold'>Solana Transaction</p>
</CardHeader>
<div>
{solData ? (
<div className='p-6'>
<div className='flex flex-col gap-6 text-sm'>
<TransactionDetail label='Network' value={solData.network} />
<Accordion
type='single'
collapsible
defaultValue='transaction-0'
>
{solData.params.map((transaction: any, index: any) => (
<AccordionItem
key={index}
value={`transaction-${index}`}
className='border-0'
>
<AccordionTrigger className='pt-0 hover:no-underline'>
<div className='flex items-center justify-between text-sm'>
<p className='text-text-secondary'>
Transaction {index + 1}
</p>
</div>
</AccordionTrigger>
<AccordionContent className='flex flex-col gap-6 border-0'>
<TransactionDetail
label='Data'
value={
<CopyStandard
text={transaction}
textSize='sm'
textColor='gray-800'
charSize={isMobile ? 10 : 15}
/>
}
/>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</div>
) : null}
</div>

{errorMsg && !isLoading ? (
<div className='flex flex-col items-center gap-4 px-6 pb-6 text-center text-sm'>
<p className='text-red-300'>
An error occurred trying to execute your transaction: {errorMsg}
.
</p>
<Button
className='w-1/2'
variant='outline'
onClick={() => {
setErrorMsg("");
}}
>
Dismiss
</Button>
</div>
) : null}

{isLoading ? <LoadingMessage /> : null}
{txResult ? (
<TransactionResult
result={{ solana: txResult }}
textColor='text-gray-800'
/>
) : null}
{!isLoading && !errorMsg && !txResult ? (
<CardFooter className='flex items-center gap-6'>
<>
<Button variant='outline' className='w-1/2'>
Decline
</Button>

<Button
className='w-1/2'
onClick={handleSmartAction}
disabled={isLoading}
>
{isLoading ? "Confirming..." : "Approve"}
</Button>
</>
</CardFooter>
) : null}
</Card>
</div>
</>
);
};

// TODO: Remove this test helper once we have proper agent integration for testing
export async function createTestTransaction(connection: Connection) {
if (!connection) {
throw new Error("Connection is required");
}

try {
// Define addresses
const fromPubkey = new PublicKey("DRpbCBMxVnDK7maPGv6MvL4SXKKBYv3vzjact1i7DKG8");
const toPubkey = new PublicKey("6fQY9fgE4zEWjkZz3XNZGwpjP1VXE9c3ZvGHkGgHqe1V");

// Create transfer instruction
const transferInstruction = SystemProgram.transfer({
fromPubkey,
toPubkey,
lamports: LAMPORTS_PER_SOL * 0.1
});

// Create and prepare transaction
const transaction = new Transaction();
const { blockhash } = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
transaction.feePayer = fromPubkey;
transaction.add(transferInstruction);

return [transaction];

} catch (error) {
console.error("Error creating test transaction:", error);
throw error;
}
}
Loading