Skip to content

Commit 3585e40

Browse files
authored
feat: Synchronous retry (#460)
* feat: Synchronous retry * try/catch sendTransaction * example * optional gas overrides
1 parent 8111007 commit 3585e40

File tree

3 files changed

+182
-0
lines changed

3 files changed

+182
-0
lines changed

src/db/transactions/getTxById.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ interface GetTxByIdParams {
99
pgtx?: PrismaTransaction;
1010
}
1111

12+
/**
13+
* @deprecated - Call prisma directly.
14+
*/
1215
export const getTxById = async ({
1316
pgtx,
1417
queueId,

src/server/routes/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ import { removeContractSubscription } from "./contract/subscriptions/removeContr
126126
import { getContractTransactionReceipts } from "./contract/transactions/getTransactionReceipts";
127127
import { getContractTransactionReceiptsByTimestamp } from "./contract/transactions/getTransactionReceiptsByTimestamp";
128128
import { pageTransactionReceipts } from "./contract/transactions/paginateTransactionReceipts";
129+
import { syncRetryTransaction } from "./transaction/syncRetry";
129130

130131
export const withRoutes = async (fastify: FastifyInstance) => {
131132
// Backend Wallets
@@ -226,6 +227,7 @@ export const withRoutes = async (fastify: FastifyInstance) => {
226227
await fastify.register(getAllDeployedContracts);
227228
await fastify.register(checkGroupStatus);
228229
await fastify.register(retryTransaction);
230+
await fastify.register(syncRetryTransaction);
229231
await fastify.register(cancelTransaction);
230232
await fastify.register(sendSignedTransaction);
231233
await fastify.register(sendSignedUserOp);
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { TransactionResponse } from "@ethersproject/abstract-provider";
2+
import { Static, Type } from "@sinclair/typebox";
3+
import { FastifyInstance } from "fastify";
4+
import { StatusCodes } from "http-status-codes";
5+
import { prisma } from "../../../db/client";
6+
import { updateTx } from "../../../db/transactions/updateTx";
7+
import { getSdk } from "../../../utils/cache/getSdk";
8+
import { msSince } from "../../../utils/date";
9+
import { parseTxError } from "../../../utils/errors";
10+
import { UsageEventTxActionEnum, reportUsage } from "../../../utils/usage";
11+
import { createCustomError } from "../../middleware/error";
12+
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";
13+
import { TransactionStatusEnum } from "../../schemas/transaction";
14+
15+
// INPUT
16+
const requestBodySchema = Type.Object({
17+
queueId: Type.String({
18+
description: "Transaction queue ID",
19+
examples: ["9eb88b00-f04f-409b-9df7-7dcc9003bc35"],
20+
}),
21+
maxFeePerGas: Type.Optional(Type.String()),
22+
maxPriorityFeePerGas: Type.Optional(Type.String()),
23+
});
24+
25+
// OUTPUT
26+
export const responseBodySchema = Type.Object({
27+
result: Type.Object({
28+
transactionHash: Type.String(),
29+
}),
30+
});
31+
32+
responseBodySchema.example = {
33+
result: {
34+
transactionHash:
35+
"0xc3b437073c164c33f95065fb325e9bc419f306cb39ae8b4ca233f33efaa74ead",
36+
},
37+
};
38+
39+
export async function syncRetryTransaction(fastify: FastifyInstance) {
40+
fastify.route<{
41+
Body: Static<typeof requestBodySchema>;
42+
Reply: Static<typeof responseBodySchema>;
43+
}>({
44+
method: "POST",
45+
url: "/transaction/sync-retry",
46+
schema: {
47+
summary: "Retry transaction (synchronous)",
48+
description:
49+
"Synchronously retry a transaction with updated gas settings.",
50+
tags: ["Transaction"],
51+
operationId: "syncRetry",
52+
body: requestBodySchema,
53+
response: {
54+
...standardResponseSchema,
55+
[StatusCodes.OK]: responseBodySchema,
56+
},
57+
},
58+
handler: async (request, reply) => {
59+
const { queueId, maxFeePerGas, maxPriorityFeePerGas } = request.body;
60+
const walletAddress = request.headers[
61+
"x-backend-wallet-address"
62+
] as string;
63+
64+
const tx = await prisma.transactions.findUnique({
65+
where: {
66+
id: queueId,
67+
},
68+
});
69+
if (!tx) {
70+
throw createCustomError(
71+
`Transaction not found with queueId ${queueId}`,
72+
StatusCodes.NOT_FOUND,
73+
"TX_NOT_FOUND",
74+
);
75+
}
76+
if (
77+
// Already mined.
78+
tx.minedAt ||
79+
// Not yet sent.
80+
!tx.sentAt ||
81+
// Missing expected values.
82+
!tx.id ||
83+
!tx.queuedAt ||
84+
!tx.chainId ||
85+
!tx.toAddress ||
86+
!tx.fromAddress ||
87+
!tx.data ||
88+
!tx.value ||
89+
!tx.nonce ||
90+
!tx.maxFeePerGas ||
91+
!tx.maxPriorityFeePerGas
92+
) {
93+
throw createCustomError(
94+
"Transaction is not in a valid state.",
95+
StatusCodes.BAD_REQUEST,
96+
"CANNOT_RETRY_TX",
97+
);
98+
}
99+
100+
// Get signer.
101+
const sdk = await getSdk({
102+
chainId: Number(tx.chainId),
103+
walletAddress,
104+
});
105+
const signer = sdk.getSigner();
106+
if (!signer) {
107+
throw createCustomError(
108+
"Backend wallet not found.",
109+
StatusCodes.BAD_REQUEST,
110+
"BACKEND_WALLET_NOT_FOUND",
111+
);
112+
}
113+
114+
const blockNumber = await sdk.getProvider().getBlockNumber();
115+
116+
// Send transaction and get the transaction hash.
117+
const txRequest = {
118+
to: tx.toAddress,
119+
from: tx.fromAddress,
120+
data: tx.data,
121+
value: tx.value,
122+
nonce: tx.nonce,
123+
maxFeePerGas: maxFeePerGas ?? tx.maxFeePerGas,
124+
maxPriorityFeePerGas: maxPriorityFeePerGas ?? tx.maxPriorityFeePerGas,
125+
};
126+
127+
let txResponse: TransactionResponse;
128+
try {
129+
txResponse = await signer.sendTransaction(txRequest);
130+
if (!txResponse) {
131+
throw "Missing transaction response.";
132+
}
133+
} catch (e) {
134+
const errorMessage = await parseTxError(tx, e);
135+
throw createCustomError(
136+
errorMessage,
137+
StatusCodes.BAD_REQUEST,
138+
"TRANSACTION_RETRY_FAILED",
139+
);
140+
}
141+
const transactionHash = txResponse.hash;
142+
143+
// Update DB.
144+
await updateTx({
145+
queueId: tx.id,
146+
data: {
147+
status: TransactionStatusEnum.Submitted,
148+
transactionHash,
149+
res: txRequest,
150+
sentAt: new Date(),
151+
sentAtBlockNumber: blockNumber,
152+
},
153+
});
154+
reportUsage([
155+
{
156+
action: UsageEventTxActionEnum.SendTx,
157+
input: {
158+
fromAddress: tx.fromAddress,
159+
toAddress: tx.toAddress,
160+
value: tx.value,
161+
chainId: tx.chainId,
162+
transactionHash,
163+
functionName: tx.functionName || undefined,
164+
extension: tx.extension || undefined,
165+
msSinceQueue: msSince(new Date(tx.queuedAt)),
166+
},
167+
},
168+
]);
169+
170+
reply.status(StatusCodes.OK).send({
171+
result: {
172+
transactionHash,
173+
},
174+
});
175+
},
176+
});
177+
}

0 commit comments

Comments
 (0)