Skip to content

Commit 8210836

Browse files
chore: moved simulation to queueRawTx (#402)
* added raw call data simulation * updated * added simulate tx to write endpoint * updated simulation * updated simulation result * added simulation endpoint * finish simulation * updated params * removed pgtx * renamed simulation -> simulate * renamed simulation -> simulate * renamed endpoint * build * updates to fix simulatTx string -> bool * added cause * updated sim error response, few others upds --------- Co-authored-by: farhanW3 <farhan@thirdweb.com>
1 parent 0c59eed commit 8210836

File tree

7 files changed

+213
-16
lines changed

7 files changed

+213
-16
lines changed

src/db/transactions/queueTx.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type {
22
DeployTransaction,
33
Transaction,
4-
TransactionError,
54
} from "@thirdweb-dev/sdk";
65
import { ERC4337EthersSigner } from "@thirdweb-dev/wallets/dist/declarations/src/evm/connectors/smart-wallet/lib/erc4337-signer";
76
import { BigNumber } from "ethers";
@@ -20,15 +19,14 @@ interface QueueTxParams {
2019
simulateTx?: boolean;
2120
}
2221

23-
// TODO: Simulation should be done before this function
2422
export const queueTx = async ({
2523
pgtx,
2624
tx,
2725
chainId,
2826
extension,
2927
deployedContractAddress,
3028
deployedContractType,
31-
simulateTx = false,
29+
simulateTx,
3230
}: QueueTxParams) => {
3331
// TODO: We need a much safer way of detecting if the transaction should be a user operation
3432
const isUserOp = !!(tx.getSigner as ERC4337EthersSigner).erc4337provider;
@@ -56,21 +54,11 @@ export const queueTx = async ({
5654
signerAddress,
5755
accountAddress,
5856
target,
57+
simulateTx
5958
});
6059

6160
return queueId;
6261
} else {
63-
try {
64-
if (!deployedContractAddress && simulateTx) {
65-
await tx.simulate();
66-
}
67-
} catch (err: any) {
68-
const errorMessage =
69-
(err as TransactionError)?.reason || (err as any).message || err;
70-
throw new Error(
71-
`Transaction simulation failed with reason: ${errorMessage}`,
72-
);
73-
}
7462
const fromAddress = await tx.getSignerAddress();
7563
const toAddress = tx.getTarget();
7664

@@ -79,6 +67,7 @@ export const queueTx = async ({
7967
...txData,
8068
fromAddress,
8169
toAddress,
70+
simulateTx
8271
});
8372

8473
return queueId;

src/db/transactions/queueTxRaw.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Prisma } from "@prisma/client";
22
import { PrismaTransaction } from "../../schema/prisma";
3+
import { simulateTx } from "../../server/utils/simulateTx";
34
import { getPrismaWithPostgresTx } from "../client";
45
import { getWalletDetails } from "../wallets/getWalletDetails";
56

@@ -8,6 +9,7 @@ type QueueTxRawParams = Omit<
89
"fromAddress" | "signerAddress"
910
> & {
1011
pgtx?: PrismaTransaction;
12+
simulateTx?: boolean;
1113
} & (
1214
| {
1315
fromAddress: string;
@@ -19,8 +21,11 @@ type QueueTxRawParams = Omit<
1921
}
2022
);
2123

22-
// TODO: Simulation should be moved to this function
23-
export const queueTxRaw = async ({ pgtx, ...tx }: QueueTxRawParams) => {
24+
export const queueTxRaw = async ({
25+
simulateTx: shouldSimulate,
26+
pgtx,
27+
...tx
28+
}: QueueTxRawParams) => {
2429
const prisma = getPrismaWithPostgresTx(pgtx);
2530

2631
const walletDetails = await getWalletDetails({
@@ -36,6 +41,10 @@ export const queueTxRaw = async ({ pgtx, ...tx }: QueueTxRawParams) => {
3641
);
3742
}
3843

44+
if (shouldSimulate) {
45+
await simulateTx({ txRaw: tx });
46+
}
47+
3948
return prisma.transactions.create({
4049
data: {
4150
...tx,

src/server/routes/backend-wallet/sendTransaction.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { FastifyInstance } from "fastify";
33
import { StatusCodes } from "http-status-codes";
44
import { queueTxRaw } from "../../../db/transactions/queueTxRaw";
55
import {
6+
requestQuerystringSchema,
67
standardResponseSchema,
78
transactionWritesResponseSchema,
89
} from "../../schemas/sharedApiSchemas";
@@ -40,6 +41,7 @@ export async function sendTransaction(fastify: FastifyInstance) {
4041
Params: Static<typeof ParamsSchema>;
4142
Body: Static<typeof requestBodySchema>;
4243
Reply: Static<typeof transactionWritesResponseSchema>;
44+
Querystring: Static<typeof requestQuerystringSchema>;
4345
}>({
4446
method: "POST",
4547
url: "/backend-wallet/:chain/send-transaction",
@@ -51,6 +53,7 @@ export async function sendTransaction(fastify: FastifyInstance) {
5153
params: ParamsSchema,
5254
body: requestBodySchema,
5355
headers: Type.Omit(walletAuthSchema, ["x-account-address"]),
56+
querystring: requestQuerystringSchema,
5457
response: {
5558
...standardResponseSchema,
5659
[StatusCodes.OK]: transactionWritesResponseSchema,
@@ -59,6 +62,7 @@ export async function sendTransaction(fastify: FastifyInstance) {
5962
handler: async (request, reply) => {
6063
const { chain } = request.params;
6164
const { toAddress, data, value } = request.body;
65+
const { simulateTx } = request.query;
6266
const fromAddress = request.headers["x-backend-wallet-address"] as string;
6367
const chainId = await getChainIdFromChain(chain);
6468

@@ -68,6 +72,7 @@ export async function sendTransaction(fastify: FastifyInstance) {
6872
toAddress,
6973
data,
7074
value,
75+
simulateTx: simulateTx,
7176
});
7277

7378
reply.status(StatusCodes.OK).send({
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { Static, Type } from "@sinclair/typebox";
2+
import { FastifyInstance } from "fastify";
3+
import { StatusCodes } from "http-status-codes";
4+
import { getContract } from "../../../utils/cache/getContract";
5+
import {
6+
simulateResponseSchema,
7+
standardResponseSchema,
8+
} from "../../schemas/sharedApiSchemas";
9+
import { walletAuthSchema } from "../../schemas/wallet";
10+
import { getChainIdFromChain } from "../../utils/chain";
11+
import { SimulateTxParams, simulateTx } from "../../utils/simulateTx";
12+
13+
// INPUT
14+
const ParamsSchema = Type.Object({
15+
chain: Type.String(),
16+
});
17+
18+
const simulateRequestBodySchema = Type.Object({
19+
toAddress: Type.String({
20+
description: "Address of the contract",
21+
}),
22+
value: Type.Optional(
23+
Type.String({
24+
examples: ["0"],
25+
description: "Native Currency Value",
26+
}),
27+
),
28+
// Decoded transaction args
29+
functionName: Type.Optional(
30+
Type.String({
31+
description: "Name of the function to call on Contract",
32+
}),
33+
),
34+
args: Type.Optional(
35+
Type.Array(
36+
Type.Union([
37+
Type.String({
38+
description: "Arguments for the function. Comma Separated",
39+
}),
40+
Type.Tuple([Type.String(), Type.String()]),
41+
Type.Object({}),
42+
Type.Array(Type.Any()),
43+
Type.Any(),
44+
]),
45+
),
46+
),
47+
// Raw transaction args
48+
data: Type.Optional(
49+
Type.String({
50+
description: "Transaction Data",
51+
}),
52+
),
53+
});
54+
55+
// LOGIC
56+
export async function simulateTransaction(fastify: FastifyInstance) {
57+
fastify.route<{
58+
Params: Static<typeof ParamsSchema>;
59+
Body: Static<typeof simulateRequestBodySchema>;
60+
Reply: Static<typeof simulateResponseSchema>;
61+
}>({
62+
method: "POST",
63+
url: "/backend-wallet/:chain/simulate-transaction",
64+
schema: {
65+
summary: "Simulate a transaction",
66+
description: "Simulate a transaction with transaction parameters",
67+
tags: ["Backend Wallet"],
68+
operationId: "simulateTransaction",
69+
params: ParamsSchema,
70+
body: simulateRequestBodySchema,
71+
headers: Type.Omit(walletAuthSchema, ["x-account-address"]),
72+
response: {
73+
...standardResponseSchema,
74+
[StatusCodes.OK]: simulateResponseSchema,
75+
},
76+
},
77+
handler: async (request, reply) => {
78+
// Destruct core params
79+
const { chain } = request.params;
80+
const { toAddress, value, functionName, args, data } = request.body;
81+
const walletAddress = request.headers[
82+
"x-backend-wallet-address"
83+
] as string;
84+
const accountAddress = request.headers["x-account-address"] as string;
85+
const chainId = await getChainIdFromChain(chain);
86+
87+
// Get decoded tx simulate args
88+
let simulateArgs: SimulateTxParams;
89+
if (functionName && args) {
90+
const contract = await getContract({
91+
chainId,
92+
contractAddress: toAddress,
93+
walletAddress,
94+
accountAddress,
95+
});
96+
const tx = contract.prepare(functionName, args, {
97+
value: value ?? "0",
98+
});
99+
simulateArgs = { tx };
100+
}
101+
// Get raw tx simulate args
102+
else {
103+
simulateArgs = {
104+
txRaw: {
105+
chainId: chainId.toString(),
106+
fromAddress: walletAddress,
107+
toAddress,
108+
data,
109+
value,
110+
},
111+
};
112+
}
113+
114+
// Simulate raw tx
115+
await simulateTx(simulateArgs);
116+
117+
// Return success
118+
reply.status(StatusCodes.OK).send({
119+
result: {
120+
success: true,
121+
},
122+
});
123+
},
124+
});
125+
}

src/server/routes/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ import { queueStatus } from "./system/queue";
110110
import { checkGroupStatus } from "./transaction/group";
111111
import { sendSignedTransaction } from "./transaction/sendSignedTx";
112112
import { sendSignedUserOp } from "./transaction/sendSignedUserOp";
113+
import { simulateTransaction } from "./backend-wallet/simulateTransaction";
113114

114115
export const withRoutes = async (fastify: FastifyInstance) => {
115116
// Backend Wallets
@@ -127,6 +128,7 @@ export const withRoutes = async (fastify: FastifyInstance) => {
127128
await fastify.register(getAllTransactions);
128129
await fastify.register(resetBackendWalletNonces);
129130
await fastify.register(getBackendWalletNonce);
131+
await fastify.register(simulateTransaction);
130132

131133
// Configuration
132134
await fastify.register(getWalletsConfiguration);

src/server/schemas/sharedApiSchemas.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ export const transactionWritesResponseSchema = Type.Object({
155155
}),
156156
});
157157

158+
export const simulateResponseSchema = Type.Object({
159+
result: Type.Object({
160+
success: Type.Boolean({
161+
description: "Simulation Success",
162+
}),
163+
}),
164+
});
165+
158166
transactionWritesResponseSchema.example = {
159167
result: {
160168
queueId: "9eb88b00-f04f-409b-9df7-7dcc9003bc35",

src/server/utils/simulateTx.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {
2+
DeployTransaction,
3+
Transaction,
4+
TransactionError,
5+
} from "@thirdweb-dev/sdk";
6+
import { ethers } from "ethers";
7+
import { getSdk } from "../../utils/cache/getSdk";
8+
import { createCustomError } from "../middleware/error";
9+
10+
type SimulateTxRawParams = {
11+
chainId: string;
12+
toAddress?: string | null;
13+
fromAddress?: string | null;
14+
data?: string | null;
15+
value?: any;
16+
};
17+
18+
const simulateTxRaw = async (args: SimulateTxRawParams) => {
19+
const sdk = await getSdk({ chainId: parseInt(args.chainId) });
20+
const simulateResult = await sdk.getProvider().call({
21+
to: `${args.toAddress}`,
22+
from: `${args.fromAddress}`,
23+
data: `${args.data}`,
24+
value: `${args.value}`,
25+
});
26+
if (simulateResult.length > 2) {
27+
// '0x' is the success result value
28+
const decoded = ethers.utils.defaultAbiCoder.decode(
29+
["string"],
30+
ethers.utils.hexDataSlice(simulateResult, 4),
31+
);
32+
throw new Error(decoded[0]);
33+
}
34+
};
35+
36+
export type SimulateTxParams = {
37+
tx?: Transaction<any> | DeployTransaction;
38+
txRaw?: SimulateTxRawParams;
39+
};
40+
41+
export const simulateTx = async ({ tx, txRaw }: SimulateTxParams) => {
42+
try {
43+
if (tx) {
44+
await tx.simulate();
45+
} else if (txRaw) {
46+
await simulateTxRaw(txRaw);
47+
} else {
48+
throw new Error("No transaction to simulate");
49+
}
50+
} catch (err) {
51+
const errorMessage =
52+
(err as TransactionError)?.reason || (err as any).message || err;
53+
throw createCustomError(
54+
`Transaction simulation failed with reason: ${errorMessage}`,
55+
400,
56+
"BAD_REQUEST",
57+
);
58+
}
59+
};

0 commit comments

Comments
 (0)