diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 3a5d3c4..dd5a1c6 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -30,7 +30,6 @@ import { z } from "zod"; import { saveUserInformation, getUserInformation, - deleteUserInformationTool, } from "@/lib/ai/tools/user-information"; import { setupAgentKit } from "@/lib/web3/agentkit/setup"; import { generateUserProfile } from "@/lib/ai/prompts/user"; diff --git a/lib/db/migrations/0000_absurd_hobgoblin.sql b/lib/db/migrations/0000_absurd_hobgoblin.sql deleted file mode 100644 index 2a6f20e..0000000 --- a/lib/db/migrations/0000_absurd_hobgoblin.sql +++ /dev/null @@ -1,156 +0,0 @@ -CREATE TABLE IF NOT EXISTS "Charge" ( - "id" text PRIMARY KEY NOT NULL, - "userId" varchar(42) NOT NULL, - "status" varchar DEFAULT 'NEW' NOT NULL, - "product" varchar DEFAULT 'STARTERKIT' NOT NULL, - "payerAddress" varchar(42), - "amount" text NOT NULL, - "currency" text NOT NULL, - "createdAt" timestamp NOT NULL, - "confirmedAt" timestamp, - "expiresAt" timestamp, - "transactionHash" text -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "Chat" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "createdAt" timestamp NOT NULL, - "title" text NOT NULL, - "userId" varchar(42) NOT NULL, - "visibility" varchar DEFAULT 'private' NOT NULL -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "Document" ( - "id" uuid DEFAULT gen_random_uuid() NOT NULL, - "createdAt" timestamp NOT NULL, - "title" text NOT NULL, - "content" text, - "text" varchar DEFAULT 'text' NOT NULL, - "userId" varchar(42) NOT NULL, - CONSTRAINT "Document_id_createdAt_pk" PRIMARY KEY("id","createdAt") -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "Message" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "chatId" uuid NOT NULL, - "role" varchar NOT NULL, - "content" json NOT NULL, - "createdAt" timestamp NOT NULL -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "StarterKit" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "creatorId" varchar(42), - "claimerId" varchar(42), - "chargeId" text, - "createdAt" timestamp NOT NULL, - "claimedAt" timestamp, - "value" bigint NOT NULL, - "balance" bigint DEFAULT 0 NOT NULL, - "deletedAt" timestamp -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "Suggestion" ( - "id" uuid DEFAULT gen_random_uuid() NOT NULL, - "documentId" uuid NOT NULL, - "documentCreatedAt" timestamp NOT NULL, - "originalText" text NOT NULL, - "suggestedText" text NOT NULL, - "description" text, - "isResolved" boolean DEFAULT false NOT NULL, - "userId" varchar(42) NOT NULL, - "createdAt" timestamp NOT NULL, - CONSTRAINT "Suggestion_id_pk" PRIMARY KEY("id") -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "User" ( - "id" varchar(42) PRIMARY KEY NOT NULL -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "UserKnowledge" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "userId" varchar(42) NOT NULL, - "type" varchar NOT NULL, - "content" json NOT NULL, - "createdAt" timestamp NOT NULL, - "deletedAt" timestamp -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "Vote" ( - "chatId" uuid NOT NULL, - "messageId" uuid NOT NULL, - "isUpvoted" boolean NOT NULL, - CONSTRAINT "Vote_chatId_messageId_pk" PRIMARY KEY("chatId","messageId") -); ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "Charge" ADD CONSTRAINT "Charge_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "Chat" ADD CONSTRAINT "Chat_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "Document" ADD CONSTRAINT "Document_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "Message" ADD CONSTRAINT "Message_chatId_Chat_id_fk" FOREIGN KEY ("chatId") REFERENCES "public"."Chat"("id") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "StarterKit" ADD CONSTRAINT "StarterKit_creatorId_User_id_fk" FOREIGN KEY ("creatorId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "StarterKit" ADD CONSTRAINT "StarterKit_claimerId_User_id_fk" FOREIGN KEY ("claimerId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "StarterKit" ADD CONSTRAINT "StarterKit_chargeId_Charge_id_fk" FOREIGN KEY ("chargeId") REFERENCES "public"."Charge"("id") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "Suggestion" ADD CONSTRAINT "Suggestion_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "Suggestion" ADD CONSTRAINT "Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk" FOREIGN KEY ("documentId","documentCreatedAt") REFERENCES "public"."Document"("id","createdAt") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "UserKnowledge" ADD CONSTRAINT "UserKnowledge_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "Vote" ADD CONSTRAINT "Vote_chatId_Chat_id_fk" FOREIGN KEY ("chatId") REFERENCES "public"."Chat"("id") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "Vote" ADD CONSTRAINT "Vote_messageId_Message_id_fk" FOREIGN KEY ("messageId") REFERENCES "public"."Message"("id") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; diff --git a/lib/db/migrations/meta/0000_snapshot.json b/lib/db/migrations/meta/0000_snapshot.json index 6869492..f90441c 100644 --- a/lib/db/migrations/meta/0000_snapshot.json +++ b/lib/db/migrations/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "d676d783-e2de-417e-8956-8efd3b9af1b0", + "id": "a3b394b5-485a-4c1b-a4cb-8097d87871df", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", diff --git a/lib/db/migrations/meta/_journal.json b/lib/db/migrations/meta/_journal.json index a3e61d1..9527088 100644 --- a/lib/db/migrations/meta/_journal.json +++ b/lib/db/migrations/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "7", - "when": 1739109815400, - "tag": "0000_absurd_hobgoblin", + "when": 1739101065473, + "tag": "0000_dizzy_madame_masque", "breakpoints": true } ] diff --git a/lib/db/queries.ts b/lib/db/queries.ts index 2b0d5f0..0b5dfc2 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -3,7 +3,6 @@ import { and, asc, desc, eq, gt, gte, inArray, isNull, sql } from "drizzle-orm"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "./schema"; - import { user, chat, @@ -18,8 +17,11 @@ import { type UserKnowledge, charge, type UserWithRelations, + safeTransaction } from "./schema"; import type { BlockKind } from "@/components/block"; +import { SafeTransaction as SafeTxType } from '@safe-global/types-kit'; + // Optionally, if not using email/pass login, you can // use the Drizzle adapter for Auth.js / NextAuth @@ -636,6 +638,74 @@ export async function getUserCharges(userId: string) { } } +export async function saveSafeTransaction({ + transactionHash, + safeAddress, + transaction, +}: { + transactionHash: string; + safeAddress: string; + transaction: SafeTxType; +}) { + try { + const existingTx = await getSafeTransactionByHash({ transactionHash }); + + if (existingTx) { + // Merge signatures from existing and new transaction data - this is an object since it came from db + const existingSignatures = new Map(Object.entries((existingTx.transaction as SafeTxType).signatures)); + // This is already a map + const newSignatures = transaction.signatures; + const mergedSignatures = Object.fromEntries(new Map([ + ...existingSignatures, + ...newSignatures + ])); + const mergedTransactionData = { + data:transaction.data, + signatures: mergedSignatures, + signatureCount: mergedSignatures.size + }; + + // Update existing transaction with merged data + return await db + .update(safeTransaction) + .set({ + transaction: mergedTransactionData, + }) + .where(eq(safeTransaction.transactionHash, transactionHash)); + } + + // Insert new transaction if it doesn't exist + return await db.insert(safeTransaction).values({ + transactionHash, + safeAddress, + signatureCount: transaction.signatures.size, + transaction, + createdAt: new Date(), + }); + } catch (error) { + console.error('Failed to save safe transaction in database'); + throw error; + } +} + +export async function getSafeTransactionByHash({ + transactionHash, +}: { + transactionHash: string; +}) { + try { + const [transaction] = await db + .select() + .from(safeTransaction) + .where(eq(safeTransaction.transactionHash, transactionHash)) + .limit(1); + + return transaction; + } catch (error) { + console.error('Failed to get safe transaction by hash from database'); + } +} + export async function getAvailableStarterKits() { try { return await db diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 27d5e86..a60c85f 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -10,7 +10,8 @@ import { foreignKey, boolean, bigint, -} from "drizzle-orm/pg-core"; + integer, +} from 'drizzle-orm/pg-core'; export const user = pgTable("User", { id: varchar("id", { length: 42 }).primaryKey().notNull(), // Ethereum address @@ -165,6 +166,15 @@ export const suggestion = pgTable( }) ); +export const safeTransaction = pgTable("SafeTransaction", { + id: uuid("id").primaryKey().notNull().defaultRandom(), + transactionHash: text("transactionHash").notNull().unique(), + safeAddress: text("safeAddress").notNull(), + transaction: json("transaction").notNull(), + signatureCount: integer("signatureCount").notNull().default(0), + createdAt: timestamp("createdAt").notNull().defaultNow(), +}); + export type Suggestion = InferSelectModel; export type UserKnowledge = InferSelectModel; @@ -172,3 +182,5 @@ export type UserKnowledge = InferSelectModel; export type StarterKit = InferSelectModel; export type Charge = InferSelectModel; + +export type SafeTransaction = InferSelectModel; diff --git a/lib/web3/agentkit/action-providers/safe/index.ts b/lib/web3/agentkit/action-providers/safe/index.ts index 92fd33b..8752bcc 100644 --- a/lib/web3/agentkit/action-providers/safe/index.ts +++ b/lib/web3/agentkit/action-providers/safe/index.ts @@ -1,13 +1,19 @@ import { ActionProvider, CreateAction, EvmWalletProvider, Network } from '@coinbase/agentkit'; import Safe, { + EthSafeTransaction, OnchainAnalyticsProps, PredictedSafeProps, - SafeAccountConfig + SafeAccountConfig, + SigningMethod } from '@safe-global/protocol-kit' import { waitForTransactionReceipt } from 'viem/actions' -import { baseSepolia } from 'viem/chains' -import { CreateSafeSchema } from './schemas'; +import { baseSepolia, base } from 'viem/chains' +import { CreateSafeSchema, CreateSafeTransactionSchema, ExecuteSafeTransactionSchema, SignSafeTransactionSchema } from './schemas'; import { z } from 'zod'; +import { saveSafeTransaction, getSafeTransactionByHash } from '@/lib/db/queries'; +import { SafeTransaction } from '@safe-global/types-kit'; +import { adjustVInSignature, EthSafeSignature } from '@safe-global/protocol-kit/dist/src/utils'; +import { PrivyWalletProvider } from '../../wallet-providers/privyWalletProvider'; const onchainAnalytics: OnchainAnalyticsProps = { project: 'HELLO_WORLD_COMPUTER', // Required. Always use the same value for your project. @@ -15,7 +21,6 @@ const onchainAnalytics: OnchainAnalyticsProps = { }; export class SafeActionProvider extends ActionProvider { - constructor() { super("safe", []); } @@ -42,6 +47,7 @@ export class SafeActionProvider extends ActionProvider { args: z.infer ): Promise { try { + const chain = process.env.NEXT_PUBLIC_ACTIVE_CHAIN === "base" ? base : baseSepolia; const safeAccountConfig: SafeAccountConfig = { owners: args.owners, threshold: args.threshold @@ -54,7 +60,7 @@ export class SafeActionProvider extends ActionProvider { }; const protocolKit = await Safe.init({ - provider: baseSepolia.rpcUrls.default.http[0], + provider: chain.rpcUrls.default.http[0], signer: walletProvider.getAddress(), predictedSafe, onchainAnalytics // Optional @@ -74,7 +80,7 @@ export class SafeActionProvider extends ActionProvider { to: deploymentTransaction.to, value: BigInt(deploymentTransaction.value), data: deploymentTransaction.data as `0x${string}`, - chain: baseSepolia + chain }); if (!tx) { @@ -100,12 +106,145 @@ export class SafeActionProvider extends ActionProvider { transactionHash, threshold: args.threshold, owners: args.owners - }; + }; } catch (error: any) { return { error }; } } + @CreateAction({ + name: "create_safe_transaction", + description: ` + This tool will create a transaction for a safe. The transaction is not executed. + It takes the following inputs: + - safeAddress: The address of the safe + - transactions: The transactions to be executed + `, + schema: CreateSafeTransactionSchema + }) + async createSafeTransaction( + walletProvider: EvmWalletProvider, + args: z.infer + ): Promise { + try { + const chain = process.env.NEXT_PUBLIC_ACTIVE_CHAIN === "base" ? base : baseSepolia; + const signerAddress = walletProvider.getAddress(); + const protocolKit = await Safe.init({ + provider: chain.rpcUrls.default.http[0], + signer: signerAddress, + safeAddress: args.safeAddress + }); + + const safeTx = await protocolKit.createTransaction({ + transactions: args.transactions + }); + const transactionHash = await protocolKit.getTransactionHash(safeTx); + // Save the transaction to the database + await saveSafeTransaction({ + transactionHash, + safeAddress: args.safeAddress, + transaction: safeTx, + }); + + // Go ahead and sign with the wallet provider + const signedTx = await signTransaction(walletProvider as PrivyWalletProvider, transactionHash); + const signatureCount = signedTx.signatures.size; + + // Store the signed transaction + await saveSafeTransaction({ + transactionHash, + safeAddress: args.safeAddress, + transaction: signedTx, + }); + + return { transactionHash, signatureCount }; + } catch (error: any) { + return { error: error.message }; + } + } + + @CreateAction({ + name: "sign_safe_transaction", + description: ` + This tool will sign a transaction for a safe. + It takes the following inputs: + - safeAddress: The address of the safe + - transactionHash: The hash of the transaction to be signed + `, + schema: SignSafeTransactionSchema + }) + async signSafeTransaction( + walletProvider: EvmWalletProvider, + args: z.infer + ): Promise { + try { + const safeTx = await signTransaction(walletProvider as PrivyWalletProvider, args.transactionHash); + const signatureCount = safeTx.signatures.size; + + await saveSafeTransaction({ + transactionHash: args.transactionHash, + safeAddress: args.safeAddress, + transaction: safeTx, + }); + + return { transactionHash: args.transactionHash, signatureCount }; + } catch (error: any) { + return { error: error.message }; + } + } + + @CreateAction({ + name: "execute_safe_transaction", + description: ` + This tool will execute a transaction for a safe assuming it has the required amount of signatures. + It takes the following inputs: + - safeAddress: The address of the safe + - transactionHash: The hash of the transaction to be executed + `, + schema: ExecuteSafeTransactionSchema + }) + async executeSafeTransaction( + walletProvider: EvmWalletProvider, + args: z.infer + ): Promise { + try { + const chain = process.env.NEXT_PUBLIC_ACTIVE_CHAIN === "base" ? base : baseSepolia; + const protocolKit = await Safe.init({ + provider: chain.rpcUrls.default.http[0], + signer: walletProvider.getAddress(), + safeAddress: args.safeAddress + }); + + const safeTx = await getTransactionByHash(args.transactionHash); + + const onchainIdentifier = protocolKit.getOnchainIdentifier(); + + const encodedTransaction = await protocolKit.getEncodedTransaction(safeTx); + + const transaction = { + to: args.safeAddress as `0x${string}`, + value: 0n, + data: encodedTransaction + onchainIdentifier as `0x${string}`, + chain + }; + + const client = + await protocolKit.getSafeProvider().getExternalSigner(); + + const prepTx = await client!.prepareTransactionRequest(transaction); + + const hash = await walletProvider.sendTransaction(prepTx); + + await waitForTransactionReceipt( + client!, + { hash } + ); + + return { transactionHash: hash }; + } catch (error: any) { + return { error: error.message }; + } + } /** * Checks if the Safe action provider supports the given network. @@ -116,4 +255,34 @@ export class SafeActionProvider extends ActionProvider { supportsNetwork = (_: Network) => true; } +const getTransactionByHash = async (transactionHash: string): Promise => { + const storedTx = await getSafeTransactionByHash({ + transactionHash + }); + + if (!storedTx) { + throw new Error("Transaction not found"); + } + + const safeTransaction = storedTx.transaction as SafeTransaction; + + const safeTx = new EthSafeTransaction(safeTransaction.data); + // Add signatures back to the transaction + const signatures = new Map(Object.entries(safeTransaction.signatures)); + for (const [signer, signature] of signatures) { + const safeSignature = new EthSafeSignature(signer, signature.data); + safeTx.addSignature(safeSignature); + } + return safeTx; +} + +const signTransaction = async (walletProvider: PrivyWalletProvider, transactionHash: string): Promise => { + const safeTx = await getTransactionByHash(transactionHash); + const signedTxHash = await walletProvider.signMessage({ raw: transactionHash as `0x${string}` }); + const signature = await adjustVInSignature(SigningMethod.ETH_SIGN, signedTxHash, transactionHash, walletProvider.getAddress()); + const safeSignature = new EthSafeSignature(walletProvider.getAddress(), signature); + safeTx.addSignature(safeSignature); + return safeTx; +} + export const safeActionProvider = () => new SafeActionProvider(); \ No newline at end of file diff --git a/lib/web3/agentkit/action-providers/safe/schemas.ts b/lib/web3/agentkit/action-providers/safe/schemas.ts index adfff7f..6bff1e3 100644 --- a/lib/web3/agentkit/action-providers/safe/schemas.ts +++ b/lib/web3/agentkit/action-providers/safe/schemas.ts @@ -14,3 +14,40 @@ export const CreateSafeSchema = z }) .strip() .describe("Instructions for creating a safe (multisig wallet)"); + +/** + * Input schema for create safe transaction action. + */ +export const CreateSafeTransactionSchema = z + .object({ + safeAddress: z.string().describe("The address of the safe"), + transactions: z.array(z.object({ + to: z.string().describe("The address of the recipient"), + value: z.string().describe("The value of the transaction. Must be a whole number. No decimals allowed. Example:0.1 ETH is 100000000000000000"), + data: z.string().describe("The data of the transaction"), + })).describe("The transactions to be executed"), + }) + .strip() + .describe("Instructions for creating a safe transaction"); + +/** + * Input schema for sign safe transaction action. + */ +export const SignSafeTransactionSchema = z + .object({ + safeAddress: z.string().describe("The address of the safe"), + transactionHash: z.string().describe("The hash of the transaction to be signed"), + }) + .strip() + .describe("Instructions for signing a safe transaction"); + +/** + * Input schema for execute safe transaction action. + */ +export const ExecuteSafeTransactionSchema = z + .object({ + safeAddress: z.string().describe("The address of the safe"), + transactionHash: z.string().describe("The hash of the transaction to be executed"), + }) + .strip() + .describe("Instructions for executing a safe transaction"); diff --git a/lib/web3/agentkit/action-providers/safe/types.ts b/lib/web3/agentkit/action-providers/safe/types.ts index 2dd7952..563c45e 100644 --- a/lib/web3/agentkit/action-providers/safe/types.ts +++ b/lib/web3/agentkit/action-providers/safe/types.ts @@ -5,4 +5,24 @@ type CreateSafeReturnType = { owners: string[] } | { error: Error; -}; \ No newline at end of file +}; + +type CreateSafeTransactionReturnType = { + transactionHash: string; + signatureCount: number; +} | { + error: Error; +}; + +type SignSafeTransactionReturnType = { + transactionHash: string; + signatureCount: number; +} | { + error: Error; +}; + +type ExecuteSafeTransactionReturnType = { + transactionHash: string; +} | { + error: Error; +}; diff --git a/lib/web3/agentkit/wallet-providers/privyWalletProvider.ts b/lib/web3/agentkit/wallet-providers/privyWalletProvider.ts index d46795a..56d0858 100644 --- a/lib/web3/agentkit/wallet-providers/privyWalletProvider.ts +++ b/lib/web3/agentkit/wallet-providers/privyWalletProvider.ts @@ -1,8 +1,18 @@ import { PrivyClient } from "@privy-io/server-auth"; import { createViemAccount } from "@privy-io/server-auth/viem"; -import { ViemWalletProvider } from "@coinbase/agentkit"; -import { createWalletClient, http, type WalletClient } from "viem"; -import { NETWORK_ID_TO_VIEM_CHAIN } from "./network"; +import { WalletProvider, Network } from "@coinbase/agentkit"; +import { + WalletClient as ViemWalletClient, + createPublicClient, + http, + TransactionRequest, + PublicClient as ViemPublicClient, + ReadContractParameters, + ReadContractReturnType, + parseEther, + createWalletClient, +} from "viem"; +import { CHAIN_ID_TO_NETWORK_ID, NETWORK_ID_TO_VIEM_CHAIN } from "./network"; interface PrivyWalletConfig { appId: string; @@ -15,11 +25,25 @@ interface PrivyWalletConfig { /** * A wallet provider that uses Privy's server wallet API. */ -export class PrivyWalletProvider extends ViemWalletProvider { - private constructor(walletClient: WalletClient) { - super(walletClient); +export class PrivyWalletProvider extends WalletProvider { + #walletClient: ViemWalletClient; + #publicClient: ViemPublicClient; + + /** + * Constructs a new ViemWalletProvider. + * + * @param walletClient - The wallet client. + */ + constructor(walletClient: ViemWalletClient) { + super(); + this.#walletClient = walletClient; + this.#publicClient = createPublicClient({ + chain: walletClient.chain, + transport: http(), + }); } + public static async configureWithWallet( config: PrivyWalletConfig ): Promise { @@ -58,4 +82,161 @@ export class PrivyWalletProvider extends ViemWalletProvider { getName(): string { return "privy_wallet_provider"; } + + /** + * Signs a message. + * + * @param message - The message to sign. + * @returns The signed message. + */ + async signMessage(message: string | { raw: `0x${string}` }): Promise<`0x${string}`> { + const account = this.#walletClient.account; + if (!account) { + throw new Error("Account not found"); + } + + return this.#walletClient.signMessage({ account, message }); + } + + /** + * Signs a typed data object. + * + * @param typedData - The typed data object to sign. + * @returns The signed typed data object. + */ + async signTypedData(typedData: any): Promise<`0x${string}`> { + return this.#walletClient.signTypedData({ + account: this.#walletClient.account!, + domain: typedData.domain!, + types: typedData.types!, + primaryType: typedData.primaryType!, + message: typedData.message!, + }); + } + + /** + * Signs a transaction. + * + * @param transaction - The transaction to sign. + * @returns The signed transaction. + */ + async signTransaction(transaction: TransactionRequest): Promise<`0x${string}`> { + const txParams = { + account: this.#walletClient.account!, + to: transaction.to, + value: transaction.value, + data: transaction.data, + chain: this.#walletClient.chain, + }; + + return this.#walletClient.signTransaction(txParams); + } + + /** + * Sends a transaction. + * + * @param transaction - The transaction to send. + * @returns The hash of the transaction. + */ + async sendTransaction(transaction: TransactionRequest): Promise<`0x${string}`> { + const account = this.#walletClient.account; + if (!account) { + throw new Error("Account not found"); + } + + const chain = this.#walletClient.chain; + if (!chain) { + throw new Error("Chain not found"); + } + + const txParams = { + account: account, + chain: chain, + data: transaction.data, + to: transaction.to, + value: transaction.value, + }; + + return this.#walletClient.sendTransaction(txParams); + } + + /** + * Gets the address of the wallet. + * + * @returns The address of the wallet. + */ + getAddress(): string { + return this.#walletClient.account?.address ?? ""; + } + + /** + * Gets the network of the wallet. + * + * @returns The network of the wallet. + */ + getNetwork(): Network { + return { + protocolFamily: "evm" as const, + chainId: String(this.#walletClient.chain!.id!), + networkId: CHAIN_ID_TO_NETWORK_ID[this.#walletClient.chain!.id!], + }; + } + + /** + * Gets the balance of the wallet. + * + * @returns The balance of the wallet. + */ + async getBalance(): Promise { + const account = this.#walletClient.account; + if (!account) { + throw new Error("Account not found"); + } + + return this.#publicClient.getBalance({ address: account.address }); + } + + /** + * Waits for a transaction receipt. + * + * @param txHash - The hash of the transaction to wait for. + * @returns The transaction receipt. + */ + async waitForTransactionReceipt(txHash: `0x${string}`): Promise { + return await this.#publicClient.waitForTransactionReceipt({ hash: txHash }); + } + + /** + * Reads a contract. + * + * @param params - The parameters to read the contract. + * @returns The response from the contract. + */ + async readContract(params: ReadContractParameters): Promise { + return this.#publicClient.readContract(params); + } + + /** + * Transfer the native asset of the network. + * + * @param to - The destination address. + * @param value - The amount to transfer in whole units (e.g. ETH) + * @returns The transaction hash. + */ + async nativeTransfer(to: `0x${string}`, value: string): Promise<`0x${string}`> { + const atomicAmount = parseEther(value); + + const tx = await this.sendTransaction({ + to: to, + value: atomicAmount, + }); + + const receipt = await this.waitForTransactionReceipt(tx); + + if (!receipt) { + throw new Error("Transaction failed"); + } + + return receipt.transactionHash; + } } diff --git a/package.json b/package.json index f9015ef..1c1b1bc 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@radix-ui/react-tooltip": "^1.1.3", "@radix-ui/react-visually-hidden": "^1.1.0", "@safe-global/protocol-kit": "^5.2.1", + "@safe-global/types-kit": "^1.0.2", "@tanstack/react-query": "^5.66.0", "@vercel/analytics": "^1.3.1", "@vercel/blob": "^0.24.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76d7f6c..2763cb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ importers: '@safe-global/protocol-kit': specifier: ^5.2.1 version: 5.2.1(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.23.8) + '@safe-global/types-kit': + specifier: ^1.0.2 + version: 1.0.2(typescript@5.6.3)(zod@3.23.8) '@tanstack/react-query': specifier: ^5.66.0 version: 5.66.0(react@19.0.0-rc-45804af1-20241021)