Skip to content

Commit c33ed20

Browse files
authored
Webhook Updates + On-Chain Tx Status Add (#314)
* added new column onChainTxStatus based on receipt status. Solved webhook multisend using locks * updates example * updated UserOp Tx Receipt update to use prisma Transactions too * updated userOp tx rceipt check. Updated getTxById to not use locking when not needed * updated load test to ignore TLS/SSL when using https for testing * updated SDK to latest to get MetaTransaction Updates * updated return type for raw query in Transactions tbl * webhooks updates for multi-serer setup. UserOp Status data added * moved sendWebhookForQueueIds post update statement
1 parent 793cbc9 commit c33ed20

File tree

19 files changed

+384
-251
lines changed

19 files changed

+384
-251
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"@t3-oss/env-core": "^0.6.0",
4444
"@thirdweb-dev/auth": "^4.1.0-nightly-c238fde8-20231020022304",
4545
"@thirdweb-dev/chains": "^0.1.55",
46-
"@thirdweb-dev/sdk": "^4.0.13",
46+
"@thirdweb-dev/sdk": "^4.0.17",
4747
"@thirdweb-dev/service-utils": "^0.4.2",
4848
"@thirdweb-dev/wallets": "^2.1.5",
4949
"body-parser": "^1.20.2",

src/db/transactions/getQueuedTxs.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Transactions } from "@prisma/client";
12
import { Static } from "@sinclair/typebox";
23
import { PrismaTransaction } from "../../schema/prisma";
34
import { transactionResponseSchema } from "../../server/schemas/transaction";
@@ -16,24 +17,23 @@ export const getQueuedTxs = async ({ pgtx }: GetQueuedTxsParams = {}): Promise<
1617
const config = await getConfiguration();
1718

1819
// TODO: Don't use env var for transactions to batch
19-
const txs = await prisma.$queryRaw`
20-
SELECT
21-
*
22-
FROM
23-
"transactions"
24-
WHERE
25-
"processedAt" IS NULL
26-
AND "sentAt" IS NULL
27-
AND "minedAt" IS NULL
28-
AND "cancelledAt" IS NULL
29-
ORDER BY
30-
"queuedAt"
31-
ASC
32-
LIMIT
33-
${config.maxTxsToProcess}
34-
FOR UPDATE SKIP LOCKED
20+
const txs = await prisma.$queryRaw<Transactions[]>`
21+
SELECT
22+
*
23+
FROM
24+
"transactions"
25+
WHERE
26+
"processedAt" IS NULL
27+
AND "sentAt" IS NULL
28+
AND "minedAt" IS NULL
29+
AND "cancelledAt" IS NULL
30+
ORDER BY
31+
"queuedAt"
32+
ASC
33+
LIMIT
34+
${config.maxTxsToProcess}
35+
FOR UPDATE SKIP LOCKED
3536
`;
3637

37-
// TODO: This might not work!
38-
return cleanTxs(txs as any);
38+
return cleanTxs(txs);
3939
};

src/db/transactions/getSentTxs.ts

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,30 +13,17 @@ export const getSentTxs = async ({ pgtx }: GetSentTxsParams = {}): Promise<
1313
const prisma = getPrismaWithPostgresTx(pgtx);
1414
const config = await getConfiguration();
1515

16-
return prisma.transactions.findMany({
17-
where: {
18-
processedAt: {
19-
not: null,
20-
},
21-
sentAt: {
22-
not: null,
23-
},
24-
transactionHash: {
25-
not: null,
26-
},
27-
accountAddress: null,
28-
minedAt: null,
29-
errorMessage: null,
30-
retryCount: {
31-
// TODO: What should the max retries be here?
32-
lt: 3,
33-
},
34-
},
35-
orderBy: [
36-
{
37-
sentAt: "asc",
38-
},
39-
],
40-
take: config.maxTxsToUpdate,
41-
});
16+
return prisma.$queryRaw<Transactions[]>`
17+
SELECT * FROM "transactions"
18+
WHERE "processedAt" IS NOT NULL
19+
AND "sentAt" IS NOT NULL
20+
AND "transactionHash" IS NOT NULL
21+
AND "accountAddress" IS NULL
22+
AND "minedAt" IS NULL
23+
AND "errorMessage" IS NULL
24+
AND "retryCount" < ${config.maxTxsToUpdate}
25+
ORDER BY "sentAt" ASC
26+
LIMIT ${config.maxTxsToUpdate}
27+
FOR UPDATE SKIP LOCKED
28+
`;
4229
};

src/db/transactions/getSentUserOps.ts

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,17 @@ export const getSentUserOps = async ({
1313
const prisma = getPrismaWithPostgresTx(pgtx);
1414
const config = await getConfiguration();
1515

16-
return prisma.transactions.findMany({
17-
where: {
18-
processedAt: {
19-
not: null,
20-
},
21-
sentAt: {
22-
not: null,
23-
},
24-
accountAddress: {
25-
not: null,
26-
},
27-
userOpHash: {
28-
not: null,
29-
},
30-
minedAt: null,
31-
errorMessage: null,
32-
retryCount: {
33-
// TODO: What should the max retries be here?
34-
lt: 3,
35-
},
36-
},
37-
orderBy: [
38-
{
39-
sentAt: "asc",
40-
},
41-
],
42-
take: config.maxTxsToUpdate,
43-
});
16+
return prisma.$queryRaw<Transactions[]>`
17+
SELECT * FROM "transactions"
18+
WHERE "processedAt" IS NOT NULL
19+
AND "sentAt" IS NOT NULL
20+
AND "accountAddress" IS NOT NULL
21+
AND "userOpHash" IS NOT NULL
22+
AND "minedAt" IS NULL
23+
AND "errorMessage" IS NULL
24+
AND "retryCount" < 3
25+
ORDER BY "sentAt" ASC
26+
LIMIT ${config.maxTxsToUpdate}
27+
FOR UPDATE SKIP LOCKED;
28+
`;
4429
};

src/db/transactions/getTxById.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ export const getTxById = async ({
1616
typeof transactionResponseSchema
1717
> | null> => {
1818
const prisma = getPrismaWithPostgresTx(pgtx);
19-
2019
const tx = await prisma.transactions.findUnique({
2120
where: {
2221
id: queueId,
2322
},
2423
});
24+
2525
if (!tx) {
2626
return null;
2727
}

src/db/transactions/getTxByIds.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Static } from "@sinclair/typebox";
2+
import { PrismaTransaction } from "../../schema/prisma";
3+
import { transactionResponseSchema } from "../../server/schemas/transaction";
4+
import { prisma } from "../client";
5+
import { cleanTxs } from "./cleanTxs";
6+
interface GetTxByIdsParams {
7+
queueIds: string[];
8+
pgtx?: PrismaTransaction;
9+
}
10+
11+
export const getTxByIds = async ({
12+
queueIds,
13+
}: GetTxByIdsParams): Promise<
14+
Static<typeof transactionResponseSchema>[] | null
15+
> => {
16+
const tx = await prisma.transactions.findMany({
17+
where: {
18+
id: {
19+
in: queueIds,
20+
},
21+
},
22+
});
23+
24+
if (!tx || tx.length === 0) {
25+
return null;
26+
}
27+
28+
const cleanedTx = cleanTxs(tx);
29+
return cleanedTx;
30+
};

src/db/transactions/updateTx.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ type UpdateTxData =
3535
gasPrice?: string;
3636
blockNumber?: number;
3737
minedAt: Date;
38+
onChainTxStatus?: number;
39+
transactionHash?: string;
40+
transactionType?: number;
41+
gasLimit?: string;
42+
maxFeePerGas?: string;
43+
maxPriorityFeePerGas?: string;
3844
};
3945

4046
export const updateTx = async ({ pgtx, queueId, data }: UpdateTxParams) => {
@@ -109,6 +115,12 @@ export const updateTx = async ({ pgtx, queueId, data }: UpdateTxParams) => {
109115
minedAt: data.minedAt,
110116
gasPrice: data.gasPrice,
111117
blockNumber: data.blockNumber,
118+
onChainTxStatus: data.onChainTxStatus,
119+
transactionHash: data.transactionHash,
120+
transactionType: data.transactionType,
121+
gasLimit: data.gasLimit,
122+
maxFeePerGas: data.maxFeePerGas,
123+
maxPriorityFeePerGas: data.maxPriorityFeePerGas,
112124
},
113125
});
114126
break;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "transactions" ADD COLUMN "onChainTxStatus" INTEGER;

src/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ model Transactions {
110110
gasPrice String? @map("gasPrice")
111111
transactionType Int? @map("transactionType")
112112
transactionHash String? @map("transactionHash")
113+
onChainTxStatus Int? @map("onChainTxStatus")
113114
// User Operation
114115
signerAddress String? @map("signerAddress")
115116
accountAddress String? @map("accountAddress")

src/server/routes/transaction/status.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ responseBodySchema.example = {
5454
errorMessage: "",
5555
txMinedTimestamp: "2023-08-25T22:42:33.000Z",
5656
blockNumber: 39398545,
57+
onChainTxStatus: 1,
5758
},
5859
};
5960

src/server/schemas/transaction/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ export const transactionResponseSchema = Type.Object({
187187
userOpHash: Type.Union([Type.String(), Type.Null()]),
188188
functionName: Type.Union([Type.String(), Type.Null()]),
189189
functionArgs: Type.Union([Type.String(), Type.Null()]),
190+
onChainTxStatus: Type.Union([Type.Number(), Type.Null()]),
190191
});
191192

192193
export enum TransactionStatusEnum {

src/server/utils/webhook.ts

Lines changed: 58 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import crypto from "crypto";
2-
import { getConfiguration } from "../../db/configuration/getConfiguration";
3-
import { getTxById } from "../../db/transactions/getTxById";
2+
import { getTxByIds } from "../../db/transactions/getTxByIds";
43
import {
54
SanitizedWebHooksSchema,
65
WalletBalanceWebhookSchema,
@@ -12,10 +11,6 @@ import { TransactionStatusEnum } from "../schemas/transaction";
1211

1312
let balanceNotificationLastSentAt = -1;
1413

15-
interface TxWebookParams {
16-
id: string;
17-
}
18-
1914
export const generateSignature = (
2015
body: Record<string, any>,
2116
timestamp: string,
@@ -74,65 +69,68 @@ export const sendWebhookRequest = async (
7469
return true;
7570
};
7671

77-
export const sendTxWebhook = async (data: TxWebookParams): Promise<void> => {
72+
export const sendTxWebhook = async (queueIds: string[]): Promise<void> => {
7873
try {
79-
const txData = await getTxById({ queueId: data.id });
80-
if (!txData) {
81-
throw new Error(`Transaction ${data.id} not found.`);
82-
}
83-
84-
let webhookConfig: SanitizedWebHooksSchema[] | undefined =
85-
await getWebhookConfig(WebhooksEventTypes.ALL_TX);
86-
87-
// For Backwards Compatibility
88-
const config = await getConfiguration();
89-
if (config?.webhookUrl && config?.webhookAuthBearerToken) {
90-
const newFormatWebhookData = {
91-
id: 0,
92-
url: config.webhookUrl,
93-
secret: config.webhookAuthBearerToken,
94-
active: true,
95-
eventType: WebhooksEventTypes.ALL_TX,
96-
createdAt: new Date().toISOString(),
97-
name: "Legacy Webhook",
98-
};
99-
await sendWebhookRequest(newFormatWebhookData, txData);
74+
const txDataByIds = await getTxByIds({ queueIds });
75+
if (!txDataByIds || txDataByIds.length === 0) {
10076
return;
10177
}
102-
103-
if (!webhookConfig) {
104-
switch (txData.status) {
105-
case TransactionStatusEnum.Queued:
106-
webhookConfig = await getWebhookConfig(WebhooksEventTypes.QUEUED_TX);
107-
break;
108-
case TransactionStatusEnum.Submitted:
109-
webhookConfig = await getWebhookConfig(WebhooksEventTypes.SENT_TX);
110-
break;
111-
case TransactionStatusEnum.Retried:
112-
webhookConfig = await getWebhookConfig(WebhooksEventTypes.RETRIED_TX);
113-
break;
114-
case TransactionStatusEnum.Mined:
115-
webhookConfig = await getWebhookConfig(WebhooksEventTypes.MINED_TX);
116-
break;
117-
case TransactionStatusEnum.Errored:
118-
webhookConfig = await getWebhookConfig(WebhooksEventTypes.ERRORED_TX);
119-
break;
120-
case TransactionStatusEnum.Cancelled:
121-
webhookConfig = await getWebhookConfig(WebhooksEventTypes.ERRORED_TX);
122-
break;
123-
}
124-
}
125-
126-
webhookConfig?.map(async (config) => {
127-
if (!config || !config?.active) {
128-
logger.server.debug("No Webhook Set or Active, skipping webhook send");
78+
for (const txData of txDataByIds!) {
79+
if (!txData) {
12980
return;
81+
} else {
82+
let webhookConfig: SanitizedWebHooksSchema[] | undefined =
83+
await getWebhookConfig(WebhooksEventTypes.ALL_TX);
84+
85+
if (!webhookConfig) {
86+
switch (txData.status) {
87+
case TransactionStatusEnum.Queued:
88+
webhookConfig = await getWebhookConfig(
89+
WebhooksEventTypes.QUEUED_TX,
90+
);
91+
break;
92+
case TransactionStatusEnum.Submitted:
93+
webhookConfig = await getWebhookConfig(
94+
WebhooksEventTypes.SENT_TX,
95+
);
96+
break;
97+
case TransactionStatusEnum.Retried:
98+
webhookConfig = await getWebhookConfig(
99+
WebhooksEventTypes.RETRIED_TX,
100+
);
101+
break;
102+
case TransactionStatusEnum.Mined:
103+
webhookConfig = await getWebhookConfig(
104+
WebhooksEventTypes.MINED_TX,
105+
);
106+
break;
107+
case TransactionStatusEnum.Errored:
108+
webhookConfig = await getWebhookConfig(
109+
WebhooksEventTypes.ERRORED_TX,
110+
);
111+
break;
112+
case TransactionStatusEnum.Cancelled:
113+
webhookConfig = await getWebhookConfig(
114+
WebhooksEventTypes.ERRORED_TX,
115+
);
116+
break;
117+
}
118+
}
119+
120+
webhookConfig?.map(async (config) => {
121+
if (!config || !config?.active) {
122+
logger.server.debug(
123+
"No Webhook Set or Active, skipping webhook send",
124+
);
125+
return;
126+
}
127+
128+
await sendWebhookRequest(config, txData);
129+
});
130130
}
131-
132-
await sendWebhookRequest(config, txData);
133-
});
131+
}
134132
} catch (error) {
135-
logger.server.error(`[sendWebhook] error: ${error}`);
133+
logger.server.error(error);
136134
}
137135
};
138136

@@ -171,6 +169,6 @@ export const sendBalanceWebhook = async (
171169
}
172170
});
173171
} catch (error) {
174-
logger.server.error(`[sendWebhook] error: ${error}`);
172+
logger.server.error(error);
175173
}
176174
};

0 commit comments

Comments
 (0)