Skip to content

Commit 8587b78

Browse files
committed
Fix batching in safes
1 parent 21cd681 commit 8587b78

3 files changed

Lines changed: 83 additions & 12 deletions

File tree

apps/evmcrispr-terminal/src/hooks/useTransactionBatcher.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TransactionAction } from "@evmcrispr/core";
1+
import type { BatchedAction } from "@evmcrispr/core";
22
import type SafeAppProvider from "@safe-global/safe-apps-sdk";
33
import { useCallback } from "react";
44
import type { Account, Chain, Transport, WalletClient } from "viem";
@@ -8,18 +8,24 @@ import { config } from "../config/wagmi";
88
export function useTransactionBatcher(safeConnector?: any) {
99
const executeBatchedActions = useCallback(
1010
async (
11-
actions: TransactionAction[],
11+
batch: BatchedAction,
1212
currentWalletClient: WalletClient<Transport, Chain, Account>,
1313
) => {
14+
const { actions, chainId } = batch;
1415
if (actions.length === 0) return;
1516

16-
const chainId = actions[0].chainId;
17+
if (
18+
actions.find(
19+
(action) =>
20+
action.chainId !== undefined && action.chainId !== chainId,
21+
)
22+
) {
23+
throw new Error("Batch contains transactions for multiple chains");
24+
}
1725

18-
if (chainId !== undefined) {
19-
const chain = config.chains.find((c) => c.id === chainId);
20-
if (chain) {
21-
await currentWalletClient.switchChain({ id: chainId });
22-
}
26+
const chain = config.chains.find((c) => c.id === chainId);
27+
if (chain) {
28+
await currentWalletClient.switchChain({ id: chainId });
2329
}
2430

2531
// Filter out contract deployments (no 'to' address) as they cannot be batched
@@ -69,7 +75,7 @@ export function useTransactionBatcher(safeConnector?: any) {
6975
);
7076

7177
const executeSafeBatchedActions = useCallback(
72-
async (actions: TransactionAction[]) => {
78+
async (batch: BatchedAction) => {
7379
if (!safeConnector)
7480
throw new Error(
7581
"Safe connector not available for Safe batched actions.",
@@ -80,8 +86,15 @@ export function useTransactionBatcher(safeConnector?: any) {
8086
if (!sdk) throw new Error("Safe SDK not available");
8187

8288
const chainId = await safeConnector.getChainId();
89+
const { actions } = batch;
8390

84-
if (actions.find((action) => action.chainId !== chainId)) {
91+
if (
92+
batch.chainId !== chainId ||
93+
actions.find(
94+
(action) =>
95+
action.chainId !== undefined && action.chainId !== chainId,
96+
)
97+
) {
8598
throw new Error("Safe does not support switching chains");
8699
}
87100

apps/evmcrispr-terminal/src/hooks/useTransactionExecutor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,9 @@ export function useTransactionExecutor(
126126
`Executing batch of ${action.actions.length} transactions from ${truncateAddress(action.from)}`,
127127
);
128128
if (safeConnector) {
129-
await executeSafeBatchedActions(action.actions);
129+
await executeSafeBatchedActions(action);
130130
} else {
131-
return await executeBatchedActions(action.actions, walletClient);
131+
return await executeBatchedActions(action, walletClient);
132132
}
133133
break;
134134
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, expect, mock, test } from "bun:test";
2+
import type { BatchedAction } from "@evmcrispr/core";
3+
import { renderHook } from "@testing-library/react";
4+
import { useTransactionBatcher } from "../../src/hooks/useTransactionBatcher";
5+
6+
const batch: BatchedAction = {
7+
type: "batched",
8+
chainId: 100,
9+
from: "0x0000000000000000000000000000000000000001",
10+
actions: [
11+
{
12+
to: "0x0000000000000000000000000000000000000002",
13+
data: "0x1234",
14+
value: 1n,
15+
},
16+
],
17+
};
18+
19+
describe("useTransactionBatcher", () => {
20+
test("submits Safe batches using the outer batch chain", async () => {
21+
const send = mock(async () => undefined);
22+
const safeConnector = {
23+
getChainId: mock(async () => 100),
24+
getProvider: mock(async () => ({
25+
sdk: { txs: { send } },
26+
})),
27+
};
28+
29+
const { result } = renderHook(() => useTransactionBatcher(safeConnector));
30+
31+
await result.current.executeSafeBatchedActions(batch);
32+
33+
expect(send).toHaveBeenCalledWith({
34+
txs: [
35+
{
36+
to: "0x0000000000000000000000000000000000000002",
37+
data: "0x1234",
38+
value: "1",
39+
},
40+
],
41+
});
42+
});
43+
44+
test("rejects Safe batches targeting a different chain", async () => {
45+
const safeConnector = {
46+
getChainId: mock(async () => 1),
47+
getProvider: mock(async () => ({
48+
sdk: { txs: { send: mock(async () => undefined) } },
49+
})),
50+
};
51+
52+
const { result } = renderHook(() => useTransactionBatcher(safeConnector));
53+
54+
await expect(
55+
result.current.executeSafeBatchedActions(batch),
56+
).rejects.toThrow("Safe does not support switching chains");
57+
});
58+
});

0 commit comments

Comments
 (0)