Skip to content

fix: Cancel tx if populating a tx to retry fails #493

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions src/db/transactions/cleanTxs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,14 @@ export const cleanTxs = (
sentAt: tx.sentAt?.toISOString() || null,
minedAt: tx.minedAt?.toISOString() || null,
cancelledAt: tx.cancelledAt?.toISOString() || null,
status: !!tx.errorMessage
status: tx.errorMessage
? "errored"
: !!tx.minedAt
: tx.minedAt
? "mined"
: !!tx.cancelledAt
: tx.cancelledAt
? "cancelled"
: !!tx.sentAt && tx.retryCount === 0
: tx.sentAt
? "sent"
: !!tx.sentAt && tx.retryCount > 0
? "retried"
: "queued",
};
});
Expand Down
38 changes: 0 additions & 38 deletions src/server/schemas/transaction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,41 +198,3 @@ export enum TransactionStatus {
// Tx was cancelled and will not be re-attempted.
Cancelled = "cancelled",
}

export interface TransactionSchema {
identifier?: string;
walletAddress?: string;
contractAddress?: string;
chainId?: string;
extension?: string;
rawFunctionName?: string;
rawFunctionArgs?: string;
txProcessed?: boolean;
txSubmitted?: boolean;
txErrored?: boolean;
txMined?: boolean;
encodedInputData?: string;
txType?: number;
gasPrice?: string;
gasLimit?: string;
maxPriorityFeePerGas?: string;
maxFeePerGas?: string;
txHash?: string;
status?: string;
createdTimestamp?: Date;
txSubmittedTimestamp?: Date;
txProcessedTimestamp?: Date;
submittedTxNonce?: number;
deployedContractAddress?: string;
contractType?: string;
txValue?: string;
errorMessage?: string;
txMinedTimestamp?: Date;
blockNumber?: number;
toAddress?: string;
txSubmittedAtBlockNumber?: number;
numberOfRetries?: number;
overrideGasValuesForTx?: boolean;
overrideMaxFeePerGas?: string;
overrideMaxPriorityFeePerGas?: string;
}
84 changes: 47 additions & 37 deletions src/server/utils/transaction.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { getDefaultGasOverrides } from "@thirdweb-dev/sdk";
import { StaticJsonRpcProvider } from "@ethersproject/providers";
import { Transactions } from "@prisma/client";
import { StatusCodes } from "http-status-codes";
import { getTxById } from "../../db/transactions/getTxById";
import { prisma } from "../../db/client";
import { updateTx } from "../../db/transactions/updateTx";
import { PrismaTransaction } from "../../schema/prisma";
import { getSdk } from "../../utils/cache/getSdk";
import { multiplyGasOverrides } from "../../utils/gas";
import { getGasSettingsForRetry } from "../../utils/gas";
import { createCustomError } from "../middleware/error";
import { TransactionStatus } from "../schemas/transaction";

Expand All @@ -17,15 +18,29 @@ export const cancelTransactionAndUpdate = async ({
queueId,
pgtx,
}: CancelTransactionAndUpdateParams) => {
const txData = await getTxById({ queueId, pgtx });
if (!txData) {
const tx = await prisma.transactions.findUnique({
where: {
id: queueId,
},
});
if (!tx) {
return {
message: `Transaction ${queueId} not found.`,
};
}

if (txData.signerAddress && txData.accountAddress) {
switch (txData.status) {
const status: TransactionStatus = tx.errorMessage
? TransactionStatus.Errored
: tx.minedAt
? TransactionStatus.Mined
: tx.cancelledAt
? TransactionStatus.Cancelled
: tx.sentAt
? TransactionStatus.Sent
: TransactionStatus.Queued;

if (tx.signerAddress && tx.accountAddress) {
switch (status) {
case TransactionStatus.Errored:
throw createCustomError(
`Cannot cancel user operation because it already errored`,
Expand Down Expand Up @@ -62,14 +77,10 @@ export const cancelTransactionAndUpdate = async ({
};
}
} else {
switch (txData.status) {
switch (status) {
case TransactionStatus.Errored: {
if (txData.chainId && txData.fromAddress && txData.nonce) {
const { message, transactionHash } = await sendNullTransaction({
chainId: parseInt(txData.chainId),
walletAddress: txData.fromAddress,
nonce: txData.nonce,
});
if (tx.chainId && tx.fromAddress && tx.nonce) {
const { message, transactionHash } = await cancelTransaction(tx);
if (transactionHash) {
await updateTx({
queueId,
Expand All @@ -84,7 +95,7 @@ export const cancelTransactionAndUpdate = async ({
}

throw createCustomError(
`Transaction has already errored: ${txData.errorMessage}`,
`Transaction has already errored: ${tx.errorMessage}`,
StatusCodes.BAD_REQUEST,
"TransactionErrored",
);
Expand Down Expand Up @@ -113,12 +124,8 @@ export const cancelTransactionAndUpdate = async ({
"TransactionAlreadyMined",
);
case TransactionStatus.Sent: {
if (txData.chainId && txData.fromAddress && txData.nonce) {
const { message, transactionHash } = await sendNullTransaction({
chainId: parseInt(txData.chainId),
walletAddress: txData.fromAddress,
nonce: txData.nonce,
});
if (tx.chainId && tx.fromAddress && tx.nonce) {
const { message, transactionHash } = await cancelTransaction(tx);
if (transactionHash) {
await updateTx({
queueId,
Expand All @@ -138,37 +145,40 @@ export const cancelTransactionAndUpdate = async ({
throw new Error("Unhandled cancellation state.");
};

const sendNullTransaction = async (args: {
chainId: number;
walletAddress: string;
nonce: number;
transactionHash?: string;
}): Promise<{
const cancelTransaction = async (
tx: Transactions,
): Promise<{
message: string;
transactionHash?: string;
}> => {
const { chainId, walletAddress, nonce, transactionHash } = args;
if (!tx.fromAddress || !tx.nonce) {
return { message: `Invalid transaction state to cancel. (${tx.id})` };
}

const sdk = await getSdk({ chainId, walletAddress });
const provider = sdk.getProvider();
const sdk = await getSdk({
chainId: parseInt(tx.chainId),
walletAddress: tx.fromAddress,
});
const provider = sdk.getProvider() as StaticJsonRpcProvider;

// Skip if the transaction is already mined.
if (transactionHash) {
const receipt = await provider.getTransactionReceipt(transactionHash);
if (tx.transactionHash) {
const receipt = await provider.getTransactionReceipt(tx.transactionHash);
if (receipt) {
return { message: "Transaction already mined." };
}
}

try {
const gasOverrides = await getDefaultGasOverrides(provider);
const gasOptions = await getGasSettingsForRetry(tx, provider);
// Send 0 currency to self.
const { hash } = await sdk.wallet.sendRawTransaction({
to: walletAddress,
from: walletAddress,
to: tx.fromAddress,
from: tx.fromAddress,
data: "0x",
value: "0",
nonce,
...multiplyGasOverrides(gasOverrides, 2),
nonce: tx.nonce,
...gasOptions,
});

return {
Expand Down
94 changes: 50 additions & 44 deletions src/worker/tasks/retryTx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ import { prisma } from "../../db/client";
import { getTxToRetry } from "../../db/transactions/getTxToRetry";
import { updateTx } from "../../db/transactions/updateTx";
import { TransactionStatus } from "../../server/schemas/transaction";
import { cancelTransactionAndUpdate } from "../../server/utils/transaction";
import { getConfig } from "../../utils/cache/getConfig";
import { getSdk } from "../../utils/cache/getSdk";
import { parseTxError } from "../../utils/errors";
import { getGasSettingsForRetry } from "../../utils/gas";
import { logger } from "../../utils/logger";
import {
ReportUsageParams,
UsageEventTxActionEnum,
reportUsage,
} from "../../utils/usage";
import { UsageEventTxActionEnum, reportUsage } from "../../utils/usage";

export const retryTx = async () => {
try {
Expand All @@ -26,13 +23,12 @@ export const retryTx = async () => {
}

const config = await getConfig();
const reportUsageForQueueIds: ReportUsageParams[] = [];
const sdk = await getSdk({
chainId: parseInt(tx.chainId!),
walletAddress: tx.fromAddress!,
});
const provider = sdk.getProvider() as StaticJsonRpcBatchProvider;
const blockNumber = await sdk.getProvider().getBlockNumber();
const blockNumber = await provider.getBlockNumber();

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

const gasOverrides = await getGasSettingsForRetry(tx, provider);
let res: ethers.providers.TransactionResponse;
const txRequest = {
const transactionRequest = {
to: tx.toAddress!,
from: tx.fromAddress!,
data: tx.data!,
nonce: tx.nonce!,
value: tx.value!,
...gasOverrides,
};

// Send transaction.
let transactionResponse: ethers.providers.TransactionResponse;
try {
res = await sdk.getSigner()!.sendTransaction(txRequest);
transactionResponse = await sdk
.getSigner()!
.sendTransaction(transactionRequest);
} catch (err: any) {
// The RPC rejected this transaction.
logger({
service: "worker",
level: "error",
queueId: tx.id,
message: `Failed to retry`,
message: "Failed to retry",
error: err,
});

// Consume the nonce.
await cancelTransactionAndUpdate({
queueId: tx.id,
pgtx,
});
await updateTx({
pgtx,
queueId: tx.id,
Expand All @@ -87,21 +93,21 @@ export const retryTx = async () => {
},
});

reportUsageForQueueIds.push({
input: {
fromAddress: tx.fromAddress || undefined,
toAddress: tx.toAddress || undefined,
value: tx.value || undefined,
chainId: tx.chainId || undefined,
functionName: tx.functionName || undefined,
extension: tx.extension || undefined,
retryCount: tx.retryCount + 1 || 0,
provider: provider.connection.url || undefined,
reportUsage([
{
input: {
fromAddress: tx.fromAddress || undefined,
toAddress: tx.toAddress || undefined,
value: tx.value || undefined,
chainId: tx.chainId,
functionName: tx.functionName || undefined,
extension: tx.extension || undefined,
retryCount: tx.retryCount + 1,
provider: provider.connection.url,
},
action: UsageEventTxActionEnum.ErrorTx,
},
action: UsageEventTxActionEnum.ErrorTx,
});

reportUsage(reportUsageForQueueIds);
]);

return;
}
Expand All @@ -112,35 +118,35 @@ export const retryTx = async () => {
data: {
sentAt: new Date(),
status: TransactionStatus.Sent,
res: txRequest,
sentAtBlockNumber: await sdk.getProvider().getBlockNumber(),
res: transactionRequest,
sentAtBlockNumber: await provider.getBlockNumber(),
retryCount: tx.retryCount + 1,
transactionHash: res.hash,
transactionHash: transactionResponse.hash,
},
});

reportUsageForQueueIds.push({
input: {
fromAddress: tx.fromAddress || undefined,
toAddress: tx.toAddress || undefined,
value: tx.value || undefined,
chainId: tx.chainId || undefined,
functionName: tx.functionName || undefined,
extension: tx.extension || undefined,
retryCount: tx.retryCount + 1,
transactionHash: res.hash || undefined,
provider: provider.connection.url || undefined,
reportUsage([
{
input: {
fromAddress: tx.fromAddress || undefined,
toAddress: tx.toAddress || undefined,
value: tx.value || undefined,
chainId: tx.chainId,
functionName: tx.functionName || undefined,
extension: tx.extension || undefined,
retryCount: tx.retryCount + 1,
transactionHash: transactionResponse.hash || undefined,
provider: provider.connection.url,
},
action: UsageEventTxActionEnum.SendTx,
},
action: UsageEventTxActionEnum.SendTx,
});

reportUsage(reportUsageForQueueIds);
]);

logger({
service: "worker",
level: "info",
queueId: tx.id,
message: `Retried with hash ${res.hash} for nonce ${res.nonce}`,
message: `Retried with hash ${transactionResponse.hash} for nonce ${transactionResponse.nonce}`,
});
},
{
Expand Down
3 changes: 2 additions & 1 deletion src/worker/tasks/updateMinedTx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { updateTx } from "../../db/transactions/updateTx";
import { TransactionStatus } from "../../server/schemas/transaction";
import { cancelTransactionAndUpdate } from "../../server/utils/transaction";
import { getSdk } from "../../utils/cache/getSdk";
import { msSince } from "../../utils/date";
import { logger } from "../../utils/logger";
import {
ReportUsageParams,
Expand Down Expand Up @@ -65,7 +66,7 @@ export const updateMinedTx = async () => {
chainId: tx.chainId || undefined,
transactionHash: tx.transactionHash || undefined,
provider: provider.connection.url || undefined,
msSinceSend: Date.now() - tx.sentAt!.getTime(),
msSinceSend: msSince(tx.sentAt!),
},
action: UsageEventTxActionEnum.CancelTx,
});
Expand Down