Skip to content

Commit a2c7f8f

Browse files
arcoravenfarhanW3
andauthored
feat: Get tx receipt from tx hash / user op hash (#440)
* Added new end-point to get tx/userOp receipt from chain * feat: Get receipt by txhash / userop hash * partial typebox --------- Co-authored-by: farhanW3 <farhan@thirdweb.com>
1 parent 03dadb3 commit a2c7f8f

File tree

6 files changed

+319
-19
lines changed

6 files changed

+319
-19
lines changed

src/server/routes/chain/get.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { Static, Type } from "@sinclair/typebox";
2-
import { allChains, minimizeChain } from "@thirdweb-dev/chains";
2+
import {
3+
getChainByChainIdAsync,
4+
getChainBySlugAsync,
5+
minimizeChain,
6+
} from "@thirdweb-dev/chains";
37
import { FastifyInstance } from "fastify";
48
import { StatusCodes } from "http-status-codes";
59
import { createCustomError } from "../../middleware/error";
@@ -55,15 +59,10 @@ export async function getChainData(fastify: FastifyInstance) {
5559
handler: async (request, reply) => {
5660
const { chain } = request.query;
5761

58-
const chainData = allChains.find((chainData) => {
59-
if (
60-
chainData.name === chain ||
61-
chainData.chainId === Number(chain) ||
62-
chainData.slug === chain
63-
) {
64-
return chain;
65-
}
66-
});
62+
let chainData = await getChainBySlugAsync(chain);
63+
if (!chainData) {
64+
chainData = await getChainByChainIdAsync(parseInt(chain));
65+
}
6766

6867
if (!chainData) {
6968
const error = createCustomError(

src/server/routes/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,11 @@ import { home } from "./home";
109109
import { updateRelayer } from "./relayer/update";
110110
import { healthCheck } from "./system/health";
111111
import { queueStatus } from "./system/queue";
112+
import { getTxHashReceipt } from "./transaction/blockchain/getTxReceipt";
113+
import { getUserOpReceipt } from "./transaction/blockchain/getUserOpReceipt";
114+
import { sendSignedTransaction } from "./transaction/blockchain/sendSignedTx";
115+
import { sendSignedUserOp } from "./transaction/blockchain/sendSignedUserOp";
112116
import { checkGroupStatus } from "./transaction/group";
113-
import { sendSignedTransaction } from "./transaction/sendSignedTx";
114-
import { sendSignedUserOp } from "./transaction/sendSignedUserOp";
115117

116118
export const withRoutes = async (fastify: FastifyInstance) => {
117119
// Backend Wallets
@@ -215,6 +217,8 @@ export const withRoutes = async (fastify: FastifyInstance) => {
215217
await fastify.register(cancelTransaction);
216218
await fastify.register(sendSignedTransaction);
217219
await fastify.register(sendSignedUserOp);
220+
await fastify.register(getTxHashReceipt);
221+
await fastify.register(getUserOpReceipt);
218222

219223
// Extensions
220224
await fastify.register(accountFactoryRoutes);
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { Static, Type } from "@sinclair/typebox";
2+
import { FastifyInstance } from "fastify";
3+
import { StatusCodes } from "http-status-codes";
4+
import { getSdk } from "../../../../utils/cache/getSdk";
5+
import { standardResponseSchema } from "../../../schemas/sharedApiSchemas";
6+
import { getChainIdFromChain } from "../../../utils/chain";
7+
8+
// INPUT
9+
const requestSchema = Type.Object({
10+
txHash: Type.String({
11+
description: "Transaction hash",
12+
examples: [
13+
"0xd9bcba8f5bc4ce5bf4d631b2a0144329c1df3b56ddb9fc64637ed3a4219dd087",
14+
],
15+
pattern: "^0x([A-Fa-f0-9]{64})$",
16+
}),
17+
chain: Type.String({
18+
examples: ["mumbai"],
19+
description: "Chain ID or name",
20+
}),
21+
});
22+
23+
// OUTPUT
24+
export const responseBodySchema = Type.Object({
25+
result: Type.Union([
26+
Type.Partial(
27+
Type.Object({
28+
to: Type.String(),
29+
from: Type.String(),
30+
contractAddress: Type.Union([Type.String(), Type.Null()]),
31+
transactionIndex: Type.Number(),
32+
root: Type.String(),
33+
gasUsed: Type.String(),
34+
logsBloom: Type.String(),
35+
blockHash: Type.String(),
36+
transactionHash: Type.String(),
37+
logs: Type.Array(Type.Any()),
38+
blockNumber: Type.Number(),
39+
confirmations: Type.Number(),
40+
cumulativeGasUsed: Type.String(),
41+
effectiveGasPrice: Type.String(),
42+
byzantium: Type.Boolean(),
43+
type: Type.Number(),
44+
status: Type.Number(),
45+
}),
46+
),
47+
Type.Null(),
48+
]),
49+
});
50+
51+
responseBodySchema.example = {
52+
result: {
53+
to: "0xd7419703c2D5737646525A8660906eCb612875BD",
54+
from: "0x9783Eb2a93A58b24CFeC56F94b30aB6e29fF4b38",
55+
contractAddress: null,
56+
transactionIndex: 69,
57+
gasUsed: "21000",
58+
logsBloom:
59+
"0x00000000000200000000000000000000000000000000000000000000000000000000000000000000000000100002000000008000000000000000000000002000000000000000000000000000000000800000000000000000000100000000040000000000000000000000000000000000008000000000000080000000000000000000000000000000000000000000000000000000000000040000000000000000200000000000004000800000000000000000000000000000000000000000004000000000000000000001000000000000000000000000800000108000000000000000000000000000000000000000000000000000000000000000008000100000",
60+
blockHash:
61+
"0x9be85de9e6a0717ed2e7c9035f7bd748a4b20bc9d6e04a6875fa69311421d971",
62+
transactionHash:
63+
"0xd9bcba8f5bc4ce5bf4d631b2a0144329c1df3b56ddb9fc64637ed3a4219dd087",
64+
logs: [
65+
{
66+
transactionIndex: 69,
67+
blockNumber: 51048531,
68+
transactionHash:
69+
"0xd9bcba8f5bc4ce5bf4d631b2a0144329c1df3b56ddb9fc64637ed3a4219dd087",
70+
address: "0x0000000000000000000000000000000000001010",
71+
topics: [
72+
"0xe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c4",
73+
"0x0000000000000000000000000000000000000000000000000000000000001010",
74+
"0x0000000000000000000000009783eb2a93a58b24cfec56f94b30ab6e29ff4b38",
75+
"0x000000000000000000000000d7419703c2d5737646525a8660906ecb612875bd",
76+
],
77+
data: "0x00000000000000000000000000000000000000000000000006a4d6a25acff49800000000000000000000000000000000000000000000000006b787e1bb9f06f80000000000000000000000000000000000000000000000051ac78aecb02246820000000000000000000000000000000000000000000000000012b13f60cf1260000000000000000000000000000000000000000000000005216c618f0af23b1a",
78+
logIndex: 181,
79+
blockHash:
80+
"0x9be85de9e6a0717ed2e7c9035f7bd748a4b20bc9d6e04a6875fa69311421d971",
81+
},
82+
{
83+
transactionIndex: 69,
84+
blockNumber: 51048531,
85+
transactionHash:
86+
"0xd9bcba8f5bc4ce5bf4d631b2a0144329c1df3b56ddb9fc64637ed3a4219dd087",
87+
address: "0x0000000000000000000000000000000000001010",
88+
topics: [
89+
"0x4dfe1bbbcf077ddc3e01291eea2d5c70c2b422b415d95645b9adcfd678cb1d63",
90+
"0x0000000000000000000000000000000000000000000000000000000000001010",
91+
"0x0000000000000000000000009783eb2a93a58b24cfec56f94b30ab6e29ff4b38",
92+
"0x000000000000000000000000a8b52f02108aa5f4b675bdcc973760022d7c6020",
93+
],
94+
data: "0x00000000000000000000000000000000000000000000000000046a2c9f9fd11800000000000000000000000000000000000000000000000006ce8dc1a70476680000000000000000000000000000000000000000000006df390338516c1391c300000000000000000000000000000000000000000000000006ca23950764a5500000000000000000000000000000000000000000000006df3907a27e0bb362db",
95+
logIndex: 182,
96+
blockHash:
97+
"0x9be85de9e6a0717ed2e7c9035f7bd748a4b20bc9d6e04a6875fa69311421d971",
98+
},
99+
],
100+
blockNumber: 51048531,
101+
confirmations: 3232643,
102+
cumulativeGasUsed: "5751865",
103+
effectiveGasPrice: "257158085297",
104+
status: 1,
105+
type: 2,
106+
byzantium: true,
107+
},
108+
};
109+
110+
export async function getTxHashReceipt(fastify: FastifyInstance) {
111+
fastify.route<{
112+
Params: Static<typeof requestSchema>;
113+
Reply: Static<typeof responseBodySchema>;
114+
}>({
115+
method: "GET",
116+
url: "/transaction/:chain/tx-hash/:txHash",
117+
schema: {
118+
summary: "Get transaction receipt from transaction hash",
119+
description: "Get the transaction receipt from a transaction hash.",
120+
tags: ["Transaction"],
121+
operationId: "txHashReceipt",
122+
params: requestSchema,
123+
response: {
124+
...standardResponseSchema,
125+
[StatusCodes.OK]: responseBodySchema,
126+
},
127+
},
128+
handler: async (request, reply) => {
129+
const { chain, txHash } = request.params;
130+
131+
const chainId = await getChainIdFromChain(chain);
132+
const sdk = await getSdk({ chainId });
133+
const receipt = await sdk.getProvider().getTransactionReceipt(txHash);
134+
135+
reply.status(StatusCodes.OK).send({
136+
result: receipt
137+
? {
138+
...receipt,
139+
gasUsed: receipt.gasUsed.toString(),
140+
cumulativeGasUsed: receipt.cumulativeGasUsed.toString(),
141+
effectiveGasPrice: receipt.effectiveGasPrice.toString(),
142+
}
143+
: null,
144+
});
145+
},
146+
});
147+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { Static, Type } from "@sinclair/typebox";
2+
import { FastifyInstance } from "fastify";
3+
import { StatusCodes } from "http-status-codes";
4+
import { env } from "../../../../utils/env";
5+
import { createCustomError } from "../../../middleware/error";
6+
import { standardResponseSchema } from "../../../schemas/sharedApiSchemas";
7+
import { getChainIdFromChain } from "../../../utils/chain";
8+
9+
// INPUT
10+
const requestSchema = Type.Object({
11+
userOpHash: Type.String({
12+
description: "User operation hash",
13+
examples: [
14+
"0xa5a579c6fd86c2d8a4d27f5bb22796614d3a31bbccaba8f3019ec001e001b95f",
15+
],
16+
pattern: "^0x([A-Fa-f0-9]{64})$",
17+
}),
18+
chain: Type.String({
19+
examples: ["mumbai"],
20+
description: "Chain ID or name",
21+
}),
22+
});
23+
24+
// OUTPUT
25+
export const responseBodySchema = Type.Object({
26+
result: Type.Union([
27+
Type.Partial(
28+
Type.Object({
29+
userOpHash: Type.String(),
30+
sender: Type.String(),
31+
nonce: Type.String(),
32+
actualGasCost: Type.String(),
33+
actualGasUsed: Type.String(),
34+
success: Type.Boolean(),
35+
paymaster: Type.String(),
36+
logs: Type.Array(Type.Any()),
37+
receipt: Type.Partial(
38+
Type.Object({
39+
type: Type.String(),
40+
status: Type.String(),
41+
cumulativeGasUsed: Type.String(),
42+
logsBloom: Type.String(),
43+
logs: Type.Array(Type.Any()),
44+
transactionHash: Type.String(),
45+
from: Type.String(),
46+
to: Type.String(),
47+
contractAddress: Type.Union([Type.String(), Type.Null()]),
48+
gasUsed: Type.String(),
49+
effectiveGasPrice: Type.String(),
50+
blockHash: Type.String(),
51+
blockNumber: Type.String(),
52+
transactionIndex: Type.Number(),
53+
blobGasUsed: Type.String(),
54+
}),
55+
),
56+
}),
57+
),
58+
Type.Null(),
59+
]),
60+
});
61+
62+
responseBodySchema.example = {
63+
result: {
64+
userOpHash:
65+
"0xa5a579c6fd86c2d8a4d27f5bb22796614d3a31bbccaba8f3019ec001e001b95f",
66+
sender: "0x8C6bdb488F664EB98E12cB2671fE2389Cc227D33",
67+
nonce: "0x18554d9a95404c5e8ac591f8608a18f80000000000000000",
68+
actualGasCost: "0x4b3b147f788710",
69+
actualGasUsed: "0x7f550",
70+
success: true,
71+
paymaster: "0xe3dc822D77f8cA7ac74c30B0dfFEA9FcDCAAA321",
72+
logs: [],
73+
receipt: {
74+
type: "eip1559",
75+
status: "success",
76+
cumulativeGasUsed: "0x4724c3",
77+
logsBloom:
78+
"0x010004000800020000000040000000000000040000000000000010000004000000080000001000000212841100000000041080000000000020000240000000000800000022001000400000080000028000040000000000200001000010000000000000000a0000000000000000800800000000004110004080800110282000000000000002000000000000000000000000000200000400000000000000240040200002000000000000400000000002000140000000000000000002200000004000000002000000000021000000000000000000000000800080108020000020000000080000000000000000000000000000000000000000000108000000102000",
79+
logs: [],
80+
transactionHash:
81+
"0x57465d20d634421008a167cfcfcde94847dba9d6b5d3652b071d4b84e5ce74ff",
82+
from: "0x43370996a3aff7b66b3ac7676dd973d01ecec039",
83+
to: "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789",
84+
contractAddress: null,
85+
gasUsed: "0x7ff5a",
86+
effectiveGasPrice: "0x89b098f46",
87+
blockHash:
88+
"0xeaeec1eff4095bdcae44d86574cf1bf08b14b26be571b7c2290f32f9f250c103",
89+
blockNumber: "0x31de70e",
90+
transactionIndex: 32,
91+
blobGasUsed: "0x0",
92+
},
93+
},
94+
};
95+
96+
export async function getUserOpReceipt(fastify: FastifyInstance) {
97+
fastify.route<{
98+
Params: Static<typeof requestSchema>;
99+
Reply: Static<typeof responseBodySchema>;
100+
}>({
101+
method: "GET",
102+
url: "/transaction/:chain/userop-hash/:userOpHash",
103+
schema: {
104+
summary: "Get transaction receipt from user-op hash",
105+
description: "Get the transaction receipt from a user-op hash.",
106+
tags: ["Transaction"],
107+
operationId: "useropHashReceipt",
108+
params: requestSchema,
109+
response: {
110+
...standardResponseSchema,
111+
[StatusCodes.OK]: responseBodySchema,
112+
},
113+
},
114+
handler: async (request, reply) => {
115+
const { chain, userOpHash } = request.params;
116+
const chainId = await getChainIdFromChain(chain);
117+
118+
try {
119+
const url = `https://${chainId}.bundler.thirdweb.com`;
120+
const resp = await fetch(url, {
121+
method: "POST",
122+
headers: {
123+
"Content-Type": "application/json",
124+
"x-secret-key": env.THIRDWEB_API_SECRET_KEY,
125+
},
126+
body: JSON.stringify({
127+
id: 0,
128+
jsonrpc: "2.0",
129+
method: "eth_getUserOperationReceipt",
130+
params: [userOpHash],
131+
}),
132+
});
133+
if (!resp.ok) {
134+
throw `Unexpected status ${resp.status} - ${await resp.text()}`;
135+
}
136+
137+
const json = await resp.json();
138+
reply.status(StatusCodes.OK).send({
139+
result: json.result,
140+
});
141+
} catch (e) {
142+
throw createCustomError(
143+
"Unable to get receipt.",
144+
StatusCodes.INTERNAL_SERVER_ERROR,
145+
"UserOpReceiptError",
146+
);
147+
}
148+
},
149+
});
150+
}

src/server/routes/transaction/sendSignedTx.ts renamed to src/server/routes/transaction/blockchain/sendSignedTx.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Static, Type } from "@sinclair/typebox";
22
import { FastifyInstance } from "fastify";
33
import { StatusCodes } from "http-status-codes";
4-
import { getSdk } from "../../../utils/cache/getSdk";
5-
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";
6-
import { getChainIdFromChain } from "../../utils/chain";
4+
import { getSdk } from "../../../../utils/cache/getSdk";
5+
import { standardResponseSchema } from "../../../schemas/sharedApiSchemas";
6+
import { getChainIdFromChain } from "../../../utils/chain";
77

88
const ParamsSchema = Type.Object({
99
chain: Type.String(),

src/server/routes/transaction/sendSignedUserOp.ts renamed to src/server/routes/transaction/blockchain/sendSignedUserOp.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { Static, Type } from "@sinclair/typebox";
22
import { Value } from "@sinclair/typebox/value";
33
import { FastifyInstance } from "fastify";
44
import { StatusCodes } from "http-status-codes";
5-
import { thirdwebClientId } from "../../../utils/api-keys";
6-
import { env } from "../../../utils/env";
7-
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";
8-
import { getChainIdFromChain } from "../../utils/chain";
5+
import { thirdwebClientId } from "../../../../utils/api-keys";
6+
import { env } from "../../../../utils/env";
7+
import { standardResponseSchema } from "../../../schemas/sharedApiSchemas";
8+
import { getChainIdFromChain } from "../../../utils/chain";
99

1010
const UserOp = Type.Object({
1111
sender: Type.String(),

0 commit comments

Comments
 (0)