Skip to content

Commit 4a97c7a

Browse files
authored
fix: Cancel tx if populating a tx to retry fails (#493)
* fix: Cancel tx if populating a tx to retry fails * ensure cancel uses high enough gas * fix build * combine prepare + send flows again
1 parent 3068b1d commit 4a97c7a

File tree

5 files changed

+103
-126
lines changed

5 files changed

+103
-126
lines changed

src/db/transactions/cleanTxs.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,14 @@ export const cleanTxs = (
1515
sentAt: tx.sentAt?.toISOString() || null,
1616
minedAt: tx.minedAt?.toISOString() || null,
1717
cancelledAt: tx.cancelledAt?.toISOString() || null,
18-
status: !!tx.errorMessage
18+
status: tx.errorMessage
1919
? "errored"
20-
: !!tx.minedAt
20+
: tx.minedAt
2121
? "mined"
22-
: !!tx.cancelledAt
22+
: tx.cancelledAt
2323
? "cancelled"
24-
: !!tx.sentAt && tx.retryCount === 0
24+
: tx.sentAt
2525
? "sent"
26-
: !!tx.sentAt && tx.retryCount > 0
27-
? "retried"
2826
: "queued",
2927
};
3028
});

src/server/schemas/transaction/index.ts

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -198,41 +198,3 @@ export enum TransactionStatus {
198198
// Tx was cancelled and will not be re-attempted.
199199
Cancelled = "cancelled",
200200
}
201-
202-
export interface TransactionSchema {
203-
identifier?: string;
204-
walletAddress?: string;
205-
contractAddress?: string;
206-
chainId?: string;
207-
extension?: string;
208-
rawFunctionName?: string;
209-
rawFunctionArgs?: string;
210-
txProcessed?: boolean;
211-
txSubmitted?: boolean;
212-
txErrored?: boolean;
213-
txMined?: boolean;
214-
encodedInputData?: string;
215-
txType?: number;
216-
gasPrice?: string;
217-
gasLimit?: string;
218-
maxPriorityFeePerGas?: string;
219-
maxFeePerGas?: string;
220-
txHash?: string;
221-
status?: string;
222-
createdTimestamp?: Date;
223-
txSubmittedTimestamp?: Date;
224-
txProcessedTimestamp?: Date;
225-
submittedTxNonce?: number;
226-
deployedContractAddress?: string;
227-
contractType?: string;
228-
txValue?: string;
229-
errorMessage?: string;
230-
txMinedTimestamp?: Date;
231-
blockNumber?: number;
232-
toAddress?: string;
233-
txSubmittedAtBlockNumber?: number;
234-
numberOfRetries?: number;
235-
overrideGasValuesForTx?: boolean;
236-
overrideMaxFeePerGas?: string;
237-
overrideMaxPriorityFeePerGas?: string;
238-
}

src/server/utils/transaction.ts

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { getDefaultGasOverrides } from "@thirdweb-dev/sdk";
1+
import { StaticJsonRpcProvider } from "@ethersproject/providers";
2+
import { Transactions } from "@prisma/client";
23
import { StatusCodes } from "http-status-codes";
3-
import { getTxById } from "../../db/transactions/getTxById";
4+
import { prisma } from "../../db/client";
45
import { updateTx } from "../../db/transactions/updateTx";
56
import { PrismaTransaction } from "../../schema/prisma";
67
import { getSdk } from "../../utils/cache/getSdk";
7-
import { multiplyGasOverrides } from "../../utils/gas";
8+
import { getGasSettingsForRetry } from "../../utils/gas";
89
import { createCustomError } from "../middleware/error";
910
import { TransactionStatus } from "../schemas/transaction";
1011

@@ -17,15 +18,29 @@ export const cancelTransactionAndUpdate = async ({
1718
queueId,
1819
pgtx,
1920
}: CancelTransactionAndUpdateParams) => {
20-
const txData = await getTxById({ queueId, pgtx });
21-
if (!txData) {
21+
const tx = await prisma.transactions.findUnique({
22+
where: {
23+
id: queueId,
24+
},
25+
});
26+
if (!tx) {
2227
return {
2328
message: `Transaction ${queueId} not found.`,
2429
};
2530
}
2631

27-
if (txData.signerAddress && txData.accountAddress) {
28-
switch (txData.status) {
32+
const status: TransactionStatus = tx.errorMessage
33+
? TransactionStatus.Errored
34+
: tx.minedAt
35+
? TransactionStatus.Mined
36+
: tx.cancelledAt
37+
? TransactionStatus.Cancelled
38+
: tx.sentAt
39+
? TransactionStatus.Sent
40+
: TransactionStatus.Queued;
41+
42+
if (tx.signerAddress && tx.accountAddress) {
43+
switch (status) {
2944
case TransactionStatus.Errored:
3045
throw createCustomError(
3146
`Cannot cancel user operation because it already errored`,
@@ -62,14 +77,10 @@ export const cancelTransactionAndUpdate = async ({
6277
};
6378
}
6479
} else {
65-
switch (txData.status) {
80+
switch (status) {
6681
case TransactionStatus.Errored: {
67-
if (txData.chainId && txData.fromAddress && txData.nonce) {
68-
const { message, transactionHash } = await sendNullTransaction({
69-
chainId: parseInt(txData.chainId),
70-
walletAddress: txData.fromAddress,
71-
nonce: txData.nonce,
72-
});
82+
if (tx.chainId && tx.fromAddress && tx.nonce) {
83+
const { message, transactionHash } = await cancelTransaction(tx);
7384
if (transactionHash) {
7485
await updateTx({
7586
queueId,
@@ -84,7 +95,7 @@ export const cancelTransactionAndUpdate = async ({
8495
}
8596

8697
throw createCustomError(
87-
`Transaction has already errored: ${txData.errorMessage}`,
98+
`Transaction has already errored: ${tx.errorMessage}`,
8899
StatusCodes.BAD_REQUEST,
89100
"TransactionErrored",
90101
);
@@ -113,12 +124,8 @@ export const cancelTransactionAndUpdate = async ({
113124
"TransactionAlreadyMined",
114125
);
115126
case TransactionStatus.Sent: {
116-
if (txData.chainId && txData.fromAddress && txData.nonce) {
117-
const { message, transactionHash } = await sendNullTransaction({
118-
chainId: parseInt(txData.chainId),
119-
walletAddress: txData.fromAddress,
120-
nonce: txData.nonce,
121-
});
127+
if (tx.chainId && tx.fromAddress && tx.nonce) {
128+
const { message, transactionHash } = await cancelTransaction(tx);
122129
if (transactionHash) {
123130
await updateTx({
124131
queueId,
@@ -138,37 +145,40 @@ export const cancelTransactionAndUpdate = async ({
138145
throw new Error("Unhandled cancellation state.");
139146
};
140147

141-
const sendNullTransaction = async (args: {
142-
chainId: number;
143-
walletAddress: string;
144-
nonce: number;
145-
transactionHash?: string;
146-
}): Promise<{
148+
const cancelTransaction = async (
149+
tx: Transactions,
150+
): Promise<{
147151
message: string;
148152
transactionHash?: string;
149153
}> => {
150-
const { chainId, walletAddress, nonce, transactionHash } = args;
154+
if (!tx.fromAddress || !tx.nonce) {
155+
return { message: `Invalid transaction state to cancel. (${tx.id})` };
156+
}
151157

152-
const sdk = await getSdk({ chainId, walletAddress });
153-
const provider = sdk.getProvider();
158+
const sdk = await getSdk({
159+
chainId: parseInt(tx.chainId),
160+
walletAddress: tx.fromAddress,
161+
});
162+
const provider = sdk.getProvider() as StaticJsonRpcProvider;
154163

155164
// Skip if the transaction is already mined.
156-
if (transactionHash) {
157-
const receipt = await provider.getTransactionReceipt(transactionHash);
165+
if (tx.transactionHash) {
166+
const receipt = await provider.getTransactionReceipt(tx.transactionHash);
158167
if (receipt) {
159168
return { message: "Transaction already mined." };
160169
}
161170
}
162171

163172
try {
164-
const gasOverrides = await getDefaultGasOverrides(provider);
173+
const gasOptions = await getGasSettingsForRetry(tx, provider);
174+
// Send 0 currency to self.
165175
const { hash } = await sdk.wallet.sendRawTransaction({
166-
to: walletAddress,
167-
from: walletAddress,
176+
to: tx.fromAddress,
177+
from: tx.fromAddress,
168178
data: "0x",
169179
value: "0",
170-
nonce,
171-
...multiplyGasOverrides(gasOverrides, 2),
180+
nonce: tx.nonce,
181+
...gasOptions,
172182
});
173183

174184
return {

src/worker/tasks/retryTx.ts

Lines changed: 50 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,13 @@ import { prisma } from "../../db/client";
44
import { getTxToRetry } from "../../db/transactions/getTxToRetry";
55
import { updateTx } from "../../db/transactions/updateTx";
66
import { TransactionStatus } from "../../server/schemas/transaction";
7+
import { cancelTransactionAndUpdate } from "../../server/utils/transaction";
78
import { getConfig } from "../../utils/cache/getConfig";
89
import { getSdk } from "../../utils/cache/getSdk";
910
import { parseTxError } from "../../utils/errors";
1011
import { getGasSettingsForRetry } from "../../utils/gas";
1112
import { logger } from "../../utils/logger";
12-
import {
13-
ReportUsageParams,
14-
UsageEventTxActionEnum,
15-
reportUsage,
16-
} from "../../utils/usage";
13+
import { UsageEventTxActionEnum, reportUsage } from "../../utils/usage";
1714

1815
export const retryTx = async () => {
1916
try {
@@ -26,13 +23,12 @@ export const retryTx = async () => {
2623
}
2724

2825
const config = await getConfig();
29-
const reportUsageForQueueIds: ReportUsageParams[] = [];
3026
const sdk = await getSdk({
3127
chainId: parseInt(tx.chainId!),
3228
walletAddress: tx.fromAddress!,
3329
});
3430
const provider = sdk.getProvider() as StaticJsonRpcBatchProvider;
35-
const blockNumber = await sdk.getProvider().getBlockNumber();
31+
const blockNumber = await provider.getBlockNumber();
3632

3733
if (
3834
blockNumber - tx.sentAtBlockNumber! <=
@@ -58,26 +54,36 @@ export const retryTx = async () => {
5854
});
5955

6056
const gasOverrides = await getGasSettingsForRetry(tx, provider);
61-
let res: ethers.providers.TransactionResponse;
62-
const txRequest = {
57+
const transactionRequest = {
6358
to: tx.toAddress!,
6459
from: tx.fromAddress!,
6560
data: tx.data!,
6661
nonce: tx.nonce!,
6762
value: tx.value!,
6863
...gasOverrides,
6964
};
65+
66+
// Send transaction.
67+
let transactionResponse: ethers.providers.TransactionResponse;
7068
try {
71-
res = await sdk.getSigner()!.sendTransaction(txRequest);
69+
transactionResponse = await sdk
70+
.getSigner()!
71+
.sendTransaction(transactionRequest);
7272
} catch (err: any) {
73+
// The RPC rejected this transaction.
7374
logger({
7475
service: "worker",
7576
level: "error",
7677
queueId: tx.id,
77-
message: `Failed to retry`,
78+
message: "Failed to retry",
7879
error: err,
7980
});
8081

82+
// Consume the nonce.
83+
await cancelTransactionAndUpdate({
84+
queueId: tx.id,
85+
pgtx,
86+
});
8187
await updateTx({
8288
pgtx,
8389
queueId: tx.id,
@@ -87,21 +93,21 @@ export const retryTx = async () => {
8793
},
8894
});
8995

90-
reportUsageForQueueIds.push({
91-
input: {
92-
fromAddress: tx.fromAddress || undefined,
93-
toAddress: tx.toAddress || undefined,
94-
value: tx.value || undefined,
95-
chainId: tx.chainId || undefined,
96-
functionName: tx.functionName || undefined,
97-
extension: tx.extension || undefined,
98-
retryCount: tx.retryCount + 1 || 0,
99-
provider: provider.connection.url || undefined,
96+
reportUsage([
97+
{
98+
input: {
99+
fromAddress: tx.fromAddress || undefined,
100+
toAddress: tx.toAddress || undefined,
101+
value: tx.value || undefined,
102+
chainId: tx.chainId,
103+
functionName: tx.functionName || undefined,
104+
extension: tx.extension || undefined,
105+
retryCount: tx.retryCount + 1,
106+
provider: provider.connection.url,
107+
},
108+
action: UsageEventTxActionEnum.ErrorTx,
100109
},
101-
action: UsageEventTxActionEnum.ErrorTx,
102-
});
103-
104-
reportUsage(reportUsageForQueueIds);
110+
]);
105111

106112
return;
107113
}
@@ -112,35 +118,35 @@ export const retryTx = async () => {
112118
data: {
113119
sentAt: new Date(),
114120
status: TransactionStatus.Sent,
115-
res: txRequest,
116-
sentAtBlockNumber: await sdk.getProvider().getBlockNumber(),
121+
res: transactionRequest,
122+
sentAtBlockNumber: await provider.getBlockNumber(),
117123
retryCount: tx.retryCount + 1,
118-
transactionHash: res.hash,
124+
transactionHash: transactionResponse.hash,
119125
},
120126
});
121127

122-
reportUsageForQueueIds.push({
123-
input: {
124-
fromAddress: tx.fromAddress || undefined,
125-
toAddress: tx.toAddress || undefined,
126-
value: tx.value || undefined,
127-
chainId: tx.chainId || undefined,
128-
functionName: tx.functionName || undefined,
129-
extension: tx.extension || undefined,
130-
retryCount: tx.retryCount + 1,
131-
transactionHash: res.hash || undefined,
132-
provider: provider.connection.url || undefined,
128+
reportUsage([
129+
{
130+
input: {
131+
fromAddress: tx.fromAddress || undefined,
132+
toAddress: tx.toAddress || undefined,
133+
value: tx.value || undefined,
134+
chainId: tx.chainId,
135+
functionName: tx.functionName || undefined,
136+
extension: tx.extension || undefined,
137+
retryCount: tx.retryCount + 1,
138+
transactionHash: transactionResponse.hash || undefined,
139+
provider: provider.connection.url,
140+
},
141+
action: UsageEventTxActionEnum.SendTx,
133142
},
134-
action: UsageEventTxActionEnum.SendTx,
135-
});
136-
137-
reportUsage(reportUsageForQueueIds);
143+
]);
138144

139145
logger({
140146
service: "worker",
141147
level: "info",
142148
queueId: tx.id,
143-
message: `Retried with hash ${res.hash} for nonce ${res.nonce}`,
149+
message: `Retried with hash ${transactionResponse.hash} for nonce ${transactionResponse.nonce}`,
144150
});
145151
},
146152
{

src/worker/tasks/updateMinedTx.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { updateTx } from "../../db/transactions/updateTx";
77
import { TransactionStatus } from "../../server/schemas/transaction";
88
import { cancelTransactionAndUpdate } from "../../server/utils/transaction";
99
import { getSdk } from "../../utils/cache/getSdk";
10+
import { msSince } from "../../utils/date";
1011
import { logger } from "../../utils/logger";
1112
import {
1213
ReportUsageParams,
@@ -65,7 +66,7 @@ export const updateMinedTx = async () => {
6566
chainId: tx.chainId || undefined,
6667
transactionHash: tx.transactionHash || undefined,
6768
provider: provider.connection.url || undefined,
68-
msSinceSend: Date.now() - tx.sentAt!.getTime(),
69+
msSinceSend: msSince(tx.sentAt!),
6970
},
7071
action: UsageEventTxActionEnum.CancelTx,
7172
});

0 commit comments

Comments
 (0)