Skip to content

Commit 7f0dc5c

Browse files
authored
Merge branch 'main' into pb/fix-scalar
2 parents 3707a89 + cb6a7c9 commit 7f0dc5c

File tree

8 files changed

+210
-106
lines changed

8 files changed

+210
-106
lines changed

src/server/middleware/error.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,17 @@ const isZodError = (err: unknown): boolean => {
5353

5454
export const withErrorHandler = async (server: FastifyInstance) => {
5555
server.setErrorHandler(
56-
(error: Error | CustomError | ZodError, request, reply) => {
56+
(error: string | Error | CustomError | ZodError, request, reply) => {
57+
if (typeof error === "string") {
58+
return reply.status(StatusCodes.INTERNAL_SERVER_ERROR).send({
59+
error: {
60+
statusCode: 500,
61+
code: "INTERNAL_SERVER_ERROR",
62+
message: error || ReasonPhrases.INTERNAL_SERVER_ERROR,
63+
},
64+
});
65+
}
66+
5767
// Ethers Error Codes
5868
if (parseEthersError(error)) {
5969
return reply.status(StatusCodes.BAD_REQUEST).send({
@@ -103,25 +113,24 @@ export const withErrorHandler = async (server: FastifyInstance) => {
103113
StatusCodes.INTERNAL_SERVER_ERROR;
104114

105115
const message = error.message ?? ReasonPhrases.INTERNAL_SERVER_ERROR;
106-
reply.status(statusCode).send({
116+
return reply.status(statusCode).send({
107117
error: {
108118
code,
109119
message,
110120
statusCode,
111121
stack: env.NODE_ENV !== "production" ? error.stack : undefined,
112122
},
113123
});
114-
} else {
115-
// Handle non-custom errors
116-
reply.status(StatusCodes.INTERNAL_SERVER_ERROR).send({
117-
error: {
118-
statusCode: 500,
119-
code: "INTERNAL_SERVER_ERROR",
120-
message: error.message || ReasonPhrases.INTERNAL_SERVER_ERROR,
121-
stack: env.NODE_ENV !== "production" ? error.stack : undefined,
122-
},
123-
});
124124
}
125+
126+
reply.status(StatusCodes.INTERNAL_SERVER_ERROR).send({
127+
error: {
128+
statusCode: 500,
129+
code: "INTERNAL_SERVER_ERROR",
130+
message: error.message || ReasonPhrases.INTERNAL_SERVER_ERROR,
131+
stack: env.NODE_ENV !== "production" ? error.stack : undefined,
132+
},
133+
});
125134
},
126135
);
127136
};

src/server/routes/contract/read/read.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Type } from "@sinclair/typebox";
22
import type { FastifyInstance } from "fastify";
33
import { StatusCodes } from "http-status-codes";
44
import { getContract } from "../../../../utils/cache/getContract";
5+
import { prettifyError } from "../../../../utils/error";
56
import { createCustomError } from "../../../middleware/error";
67
import {
78
readRequestQuerySchema,
@@ -64,22 +65,16 @@ export async function readContract(fastify: FastifyInstance) {
6465
});
6566

6667
let returnData: unknown;
67-
6868
try {
6969
returnData = await contract.call(functionName, parsedArgs ?? []);
7070
} catch (e) {
71-
if (
72-
e instanceof Error &&
73-
(e.message.includes("is not a function") ||
74-
e.message.includes("arguments, but"))
75-
) {
76-
throw createCustomError(
77-
e.message,
78-
StatusCodes.BAD_REQUEST,
79-
"BAD_REQUEST",
80-
);
81-
}
71+
throw createCustomError(
72+
prettifyError(e),
73+
StatusCodes.BAD_REQUEST,
74+
"BAD_REQUEST",
75+
);
8276
}
77+
8378
returnData = bigNumberReplacer(returnData);
8479

8580
reply.status(StatusCodes.OK).send({

src/server/routes/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ import { createWebhookRoute } from "./webhooks/create";
109109
import { getWebhooksEventTypes } from "./webhooks/events";
110110
import { getAllWebhooksData } from "./webhooks/getAll";
111111
import { revokeWebhook } from "./webhooks/revoke";
112+
import { testWebhookRoute } from "./webhooks/test";
112113

113114
export const withRoutes = async (fastify: FastifyInstance) => {
114115
// Backend Wallets
@@ -158,6 +159,7 @@ export const withRoutes = async (fastify: FastifyInstance) => {
158159
await fastify.register(createWebhookRoute);
159160
await fastify.register(revokeWebhook);
160161
await fastify.register(getWebhooksEventTypes);
162+
await fastify.register(testWebhookRoute);
161163

162164
// Permissions
163165
await fastify.register(getAllPermissions);

src/server/routes/transaction/retry-failed.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,9 @@ export async function retryFailedTransaction(fastify: FastifyInstance) {
6969
);
7070
}
7171

72-
// temp do not handle userop
7372
if (transaction.isUserOp) {
7473
throw createCustomError(
75-
`Transaction cannot be retried because it is a userop`,
74+
"Transaction cannot be retried because it is a userop",
7675
StatusCodes.BAD_REQUEST,
7776
"TRANSACTION_CANNOT_BE_RETRIED",
7877
);

src/server/routes/webhooks/test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { Type, type Static } from "@sinclair/typebox";
2+
import type { FastifyInstance } from "fastify";
3+
import { StatusCodes } from "http-status-codes";
4+
import { getWebhook } from "../../../db/webhooks/getWebhook";
5+
import { sendWebhookRequest } from "../../../utils/webhook";
6+
import { createCustomError } from "../../middleware/error";
7+
import { NumberStringSchema } from "../../schemas/number";
8+
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";
9+
import type { TransactionSchema } from "../../schemas/transaction";
10+
11+
const paramsSchema = Type.Object({
12+
webhookId: NumberStringSchema,
13+
});
14+
15+
const responseBodySchema = Type.Object({
16+
result: Type.Object({
17+
ok: Type.Boolean(),
18+
status: Type.Number(),
19+
body: Type.String(),
20+
}),
21+
});
22+
23+
export async function testWebhookRoute(fastify: FastifyInstance) {
24+
fastify.route<{
25+
Params: Static<typeof paramsSchema>;
26+
Reply: Static<typeof responseBodySchema>;
27+
}>({
28+
method: "POST",
29+
url: "/webhooks/:webhookId/test",
30+
schema: {
31+
summary: "Test webhook",
32+
description: "Send a test payload to a webhook.",
33+
tags: ["Webhooks"],
34+
operationId: "testWebhook",
35+
params: paramsSchema,
36+
response: {
37+
...standardResponseSchema,
38+
[StatusCodes.OK]: responseBodySchema,
39+
},
40+
},
41+
handler: async (req, res) => {
42+
const { webhookId } = req.params;
43+
44+
const webhook = await getWebhook(Number.parseInt(webhookId));
45+
if (!webhook) {
46+
throw createCustomError(
47+
"Webhook not found.",
48+
StatusCodes.BAD_REQUEST,
49+
"NOT_FOUND",
50+
);
51+
}
52+
53+
const webhookBody: Static<typeof TransactionSchema> = {
54+
// Queue details
55+
queueId: "1411246e-b1c8-4f5d-9a25-8c1f40b54e55",
56+
status: "mined",
57+
onchainStatus: "success",
58+
queuedAt: "2023-09-29T22:01:31.031Z",
59+
sentAt: "2023-09-29T22:01:41.580Z",
60+
minedAt: "2023-09-29T22:01:44.000Z",
61+
errorMessage: null,
62+
cancelledAt: null,
63+
retryCount: 0,
64+
65+
// Onchain details
66+
chainId: "80002",
67+
fromAddress: "0x3ecdbf3b911d0e9052b64850693888b008e18373",
68+
toAddress: "0x365b83d67d5539c6583b9c0266a548926bf216f4",
69+
data: "0xa9059cbb0000000000000000000000003ecdbf3b911d0e9052b64850693888b008e183730000000000000000000000000000000000000000000000000000000000000064",
70+
value: "0x00",
71+
nonce: 1786,
72+
gasLimit: "39580",
73+
maxFeePerGas: "2063100466",
74+
maxPriorityFeePerGas: "1875545856",
75+
gasPrice: "1875545871",
76+
transactionType: 2,
77+
transactionHash:
78+
"0xc3ffa42dd4734b017d483e1158a2e936c8a97dd1aa4e4ce11df80ac8e81d2c7e",
79+
sentAtBlockNumber: 40660021,
80+
blockNumber: 40660026,
81+
82+
// User operation (account abstraction) details
83+
signerAddress: null,
84+
accountAddress: null,
85+
accountFactoryAddress: null,
86+
target: null,
87+
sender: null,
88+
initCode: null,
89+
callData: null,
90+
callGasLimit: null,
91+
verificationGasLimit: null,
92+
preVerificationGas: null,
93+
paymasterAndData: null,
94+
userOpHash: null,
95+
accountSalt: null,
96+
97+
// Off-chain details
98+
functionName: "transfer",
99+
functionArgs: "0x3ecdbf3b911d0e9052b64850693888b008e18373,100",
100+
extension: "none",
101+
deployedContractAddress: null,
102+
deployedContractType: null,
103+
104+
// Deprecated
105+
retryGasValues: null,
106+
retryMaxFeePerGas: null,
107+
retryMaxPriorityFeePerGas: null,
108+
effectiveGasPrice: null,
109+
cumulativeGasUsed: null,
110+
onChainTxStatus: 1,
111+
};
112+
113+
const resp = await sendWebhookRequest(webhook, webhookBody);
114+
115+
res.status(StatusCodes.OK).send({
116+
result: resp,
117+
});
118+
},
119+
});
120+
}

src/server/schemas/transaction/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,10 @@ export const toTransactionSchema = (
292292
toAddress: transaction.to ?? null,
293293
data: transaction.data ?? null,
294294
value: transaction.value.toString(),
295-
nonce: "nonce" in transaction ? transaction.nonce : null,
295+
nonce:
296+
"nonce" in transaction && transaction.nonce !== undefined
297+
? transaction.nonce
298+
: null,
296299
deployedContractAddress: transaction.deployedContractAddress ?? null,
297300
deployedContractType: transaction.deployedContractType ?? null,
298301
functionName: transaction.functionName ?? null,

src/worker/tasks/nonceResyncWorker.ts

Lines changed: 35 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from "../../db/wallets/walletNonce";
99
import { getConfig } from "../../utils/cache/getConfig";
1010
import { getChain } from "../../utils/chain";
11+
import { prettifyError } from "../../utils/error";
1112
import { logger } from "../../utils/logger";
1213
import { redis } from "../../utils/redis/redis";
1314
import { thirdwebClient } from "../../utils/sdk";
@@ -40,78 +41,49 @@ export const initNonceResyncWorker = async () => {
4041
*/
4142
const handler: Processor<any, void, string> = async (job: Job<string>) => {
4243
const sentNoncesKeys = await redis.keys("nonce-sent*");
43-
job.log(`Found ${sentNoncesKeys.length} nonce-sent* keys`);
44+
if (sentNoncesKeys.length === 0) {
45+
job.log("No active wallets.");
46+
return;
47+
}
4448

4549
for (const sentNonceKey of sentNoncesKeys) {
46-
const { chainId, walletAddress } = splitSentNoncesKey(sentNonceKey);
47-
48-
const rpcRequest = getRpcClient({
49-
client: thirdwebClient,
50-
chain: await getChain(chainId),
51-
});
50+
try {
51+
const { chainId, walletAddress } = splitSentNoncesKey(sentNonceKey);
5252

53-
const [transactionCount, lastUsedNonceDb] = await Promise.all([
54-
eth_getTransactionCount(rpcRequest, {
55-
address: walletAddress,
56-
blockTag: "latest",
57-
}),
58-
inspectNonce(chainId, walletAddress),
59-
]);
60-
61-
if (Number.isNaN(transactionCount)) {
62-
job.log(
63-
`Received invalid onchain transaction count for ${walletAddress}: ${transactionCount}`,
64-
);
65-
logger({
66-
level: "error",
67-
message: `[nonceResyncWorker] Received invalid onchain transaction count for ${walletAddress}: ${transactionCount}`,
68-
service: "worker",
53+
const rpcRequest = getRpcClient({
54+
client: thirdwebClient,
55+
chain: await getChain(chainId),
6956
});
70-
continue;
71-
}
57+
const lastUsedNonceOnchain =
58+
(await eth_getTransactionCount(rpcRequest, {
59+
address: walletAddress,
60+
blockTag: "latest",
61+
})) - 1;
62+
const lastUsedNonceDb = await inspectNonce(chainId, walletAddress);
7263

73-
const lastUsedNonceOnchain = transactionCount - 1;
74-
75-
job.log(
76-
`${walletAddress} last used onchain nonce: ${lastUsedNonceOnchain} and last used db nonce: ${lastUsedNonceDb}`,
77-
);
78-
logger({
79-
level: "debug",
80-
message: `[nonceResyncWorker] last used onchain nonce: ${transactionCount} and last used db nonce: ${lastUsedNonceDb}`,
81-
service: "worker",
82-
});
83-
84-
// If the last used nonce onchain is the same as or ahead of the last used nonce in the db,
85-
// There is no need to resync the nonce.
86-
if (lastUsedNonceOnchain >= lastUsedNonceDb) {
87-
job.log(`No need to resync nonce for ${walletAddress}`);
88-
logger({
89-
level: "debug",
90-
message: `[nonceResyncWorker] No need to resync nonce for ${walletAddress}`,
91-
service: "worker",
92-
});
93-
continue;
94-
}
64+
// Recycle all nonces between (onchain nonce, db nonce] if they aren't in-flight ("sent nonce").
65+
const recycled: number[] = [];
66+
for (
67+
let nonce = lastUsedNonceOnchain + 1;
68+
nonce <= lastUsedNonceDb;
69+
nonce++
70+
) {
71+
const exists = await isSentNonce(chainId, walletAddress, nonce);
72+
if (!exists) {
73+
await recycleNonce(chainId, walletAddress, nonce);
74+
recycled.push(nonce);
75+
}
76+
}
9577

96-
// for each nonce between last used db nonce and last used onchain nonce
97-
// check if nonce exists in nonce-sent set
98-
// if it does not exist, recycle it
99-
for (
100-
let _nonce = lastUsedNonceOnchain + 1;
101-
_nonce < lastUsedNonceDb;
102-
_nonce++
103-
) {
104-
const exists = await isSentNonce(chainId, walletAddress, _nonce);
78+
const message = `wallet=${chainId}:${walletAddress} lastUsedNonceOnchain=${lastUsedNonceOnchain} lastUsedNonceDb=${lastUsedNonceDb}, recycled=${recycled.join(",")}`;
79+
job.log(message);
80+
logger({ level: "debug", service: "worker", message });
81+
} catch (error) {
10582
logger({
106-
level: "debug",
107-
message: `[nonceResyncWorker] nonce ${_nonce} exists in nonce-sent set: ${exists}`,
83+
level: "error",
84+
message: `[nonceResyncWorker] ${prettifyError(error)}`,
10885
service: "worker",
10986
});
110-
111-
// If nonce does not exist in nonce-sent set, recycle it
112-
if (!exists) {
113-
await recycleNonce(chainId, walletAddress, _nonce);
114-
}
11587
}
11688
}
11789
};

0 commit comments

Comments
 (0)