Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
escottalexander committed Feb 7, 2025
1 parent 2f2bf46 commit 50673d4
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 8 deletions.
69 changes: 69 additions & 0 deletions lib/db/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import {
type Message,
message,
vote,
safeTransaction,
} from './schema';
import { BlockKind } from '@/components/block';
import { SafeTransaction as SafeTxType, SafeMultisigTransactionResponse } from '@safe-global/types-kit';

// Optionally, if not using email/pass login, you can
// use the Drizzle adapter for Auth.js / NextAuth
Expand Down Expand Up @@ -350,3 +352,70 @@ export async function updateChatVisiblityById({
throw error;
}
}

export async function saveSafeTransaction({
transactionHash,
safeAddress,
transactionData,
}: {
transactionHash: string;
safeAddress: string;
transactionData: SafeTxType | SafeMultisigTransactionResponse;
}) {
try {
const existingTx = await getSafeTransactionByHash({ transactionHash });

if (existingTx) {
// Merge signatures from existing and new transaction data
const existingSignatures = (existingTx.transactionData as SafeTxType).signatures || {};
const newSignatures = transactionData.signatures || {};

const mergedTransactionData = {
...transactionData,
signatures: {
...existingSignatures,
...newSignatures
}
};

// Update existing transaction with merged data
return await db
.update(safeTransaction)
.set({
transactionData: mergedTransactionData,
})
.where(eq(safeTransaction.transactionHash, transactionHash));
}

// Insert new transaction if it doesn't exist
return await db.insert(safeTransaction).values({
transactionHash,
safeAddress,
signatureCount: Object.keys(transactionData?.signatures || {}).length,
transactionData,
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');
throw error;
}
}
12 changes: 12 additions & 0 deletions lib/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
primaryKey,
foreignKey,
boolean,
integer,
} from 'drizzle-orm/pg-core';

export const user = pgTable("User", {
Expand Down Expand Up @@ -111,3 +112,14 @@ export const suggestion = pgTable(
);

export type Suggestion = InferSelectModel<typeof suggestion>;

export const safeTransaction = pgTable("SafeTransaction", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
transactionHash: text("transactionHash").notNull().unique(),
safeAddress: text("safeAddress").notNull(),
transactionData: json("transactionData").notNull(),
signatureCount: integer("signatureCount").notNull().default(0),
createdAt: timestamp("createdAt").notNull().defaultNow(),
});

export type SafeTransaction = InferSelectModel<typeof safeTransaction>;
129 changes: 125 additions & 4 deletions lib/web3/agentkit/action-providers/safe/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@ import { ActionProvider, CreateAction, EvmWalletProvider, Network } from '@coinb
import Safe, {
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 { CreateSafeSchema, CreateSafeTransactionSchema, ExecuteSafeTransactionSchema, SignSafeTransactionSchema } from './schemas';
import { z } from 'zod';
import { saveSafeTransaction, getSafeTransactionByHash } from '@/lib/db/queries';
import { SafeTransaction, SafeMultisigTransactionResponse } from '@safe-global/types-kit';

const onchainAnalytics: OnchainAnalyticsProps = {
project: 'HELLO_WORLD_COMPUTER', // Required. Always use the same value for your project.
platform: 'WEB' // Optional
};

export class SafeActionProvider extends ActionProvider {

constructor() {
super("safe", []);
}
Expand Down Expand Up @@ -52,7 +54,7 @@ export class SafeActionProvider extends ActionProvider {
safeAccountConfig
// ...
};

const protocolKit = await Safe.init({
provider: baseSepolia.rpcUrls.default.http[0],
signer: walletProvider.getAddress(),
Expand Down Expand Up @@ -101,6 +103,125 @@ export class SafeActionProvider extends ActionProvider {
}
}

@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<typeof CreateSafeTransactionSchema>
): Promise<CreateSafeTransactionReturnType> {
try {
const protocolKit = await Safe.init({
provider: baseSepolia.rpcUrls.default.http[0],
signer: walletProvider.getAddress(),
safeAddress: args.safeAddress
});

const safeTx = await protocolKit.createTransaction({
transactions: args.transactions
});
// Sign the transaction as owner
const signedSafeTransaction = await protocolKit.signTransaction(safeTx, SigningMethod.ETH_SIGN_TYPED_DATA);
const transactionHash = await protocolKit.getTransactionHash(safeTx);

// Store the signed transaction using the new query method
await saveSafeTransaction({
transactionHash,
safeAddress: args.safeAddress,
transactionData: signedSafeTransaction,
});

return { transactionHash, signatureCount: Object.keys(signedSafeTransaction.signatures).length };
} 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<typeof SignSafeTransactionSchema>
): Promise<SignSafeTransactionReturnType> {
try {
const protocolKit = await Safe.init({
provider: baseSepolia.rpcUrls.default.http[0],
signer: walletProvider.getAddress(),
safeAddress: args.safeAddress
});

const storedTx = await getSafeTransactionByHash({
transactionHash: args.transactionHash
});

const txResponse = await protocolKit.signTransaction(storedTx.transactionData as SafeTransaction | SafeMultisigTransactionResponse, SigningMethod.ETH_SIGN_TYPED_DATA);

await saveSafeTransaction({
transactionHash: args.transactionHash,
safeAddress: args.safeAddress,
transactionData: txResponse,
});

const signatureCount = Object.keys(txResponse.signatures).length;

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<typeof ExecuteSafeTransactionSchema>
): Promise<ExecuteSafeTransactionReturnType> {
try {
const protocolKit = await Safe.init({
provider: baseSepolia.rpcUrls.default.http[0],
signer: walletProvider.getAddress(),
safeAddress: args.safeAddress
});

// Get the transaction using the new query method
const storedTx = await getSafeTransactionByHash({
transactionHash: args.transactionHash
});

if (!storedTx) {
throw new Error("Transaction not found");
}

const txResponse = await protocolKit.executeTransaction(storedTx.transactionData as SafeTransaction | SafeMultisigTransactionResponse);

return { transactionHash: txResponse.hash };
} catch (error: any) {
return { error: error.message };
}
}

/**
* Checks if the Safe action provider supports the given network.
Expand Down
37 changes: 37 additions & 0 deletions lib/web3/agentkit/action-providers/safe/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
22 changes: 21 additions & 1 deletion lib/web3/agentkit/action-providers/safe/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,24 @@ type CreateSafeReturnType = {
owners: string[]
} | {
error: Error;
};
};

type CreateSafeTransactionReturnType = {
transactionHash: string;
signatureCount: number;
} | {
error: Error;
};

type SignSafeTransactionReturnType = {
transactionHash: string;
signatureCount: number;
} | {
error: Error;
};

type ExecuteSafeTransactionReturnType = {
transactionHash: string;
} | {
error: Error;
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 5 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 50673d4

Please sign in to comment.