Skip to content

Commit

Permalink
feat: Get tx receipt from tx hash / user op hash (#440)
Browse files Browse the repository at this point in the history
* Added new end-point to get tx/userOp receipt from chain

* feat: Get receipt by txhash / userop hash

* partial typebox

---------

Co-authored-by: farhanW3 <[email protected]>
  • Loading branch information
arcoraven and farhanW3 authored Mar 5, 2024
1 parent 03dadb3 commit a2c7f8f
Show file tree
Hide file tree
Showing 6 changed files with 319 additions and 19 deletions.
19 changes: 9 additions & 10 deletions src/server/routes/chain/get.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Static, Type } from "@sinclair/typebox";
import { allChains, minimizeChain } from "@thirdweb-dev/chains";
import {
getChainByChainIdAsync,
getChainBySlugAsync,
minimizeChain,
} from "@thirdweb-dev/chains";
import { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import { createCustomError } from "../../middleware/error";
Expand Down Expand Up @@ -55,15 +59,10 @@ export async function getChainData(fastify: FastifyInstance) {
handler: async (request, reply) => {
const { chain } = request.query;

const chainData = allChains.find((chainData) => {
if (
chainData.name === chain ||
chainData.chainId === Number(chain) ||
chainData.slug === chain
) {
return chain;
}
});
let chainData = await getChainBySlugAsync(chain);
if (!chainData) {
chainData = await getChainByChainIdAsync(parseInt(chain));
}

if (!chainData) {
const error = createCustomError(
Expand Down
8 changes: 6 additions & 2 deletions src/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,11 @@ import { home } from "./home";
import { updateRelayer } from "./relayer/update";
import { healthCheck } from "./system/health";
import { queueStatus } from "./system/queue";
import { getTxHashReceipt } from "./transaction/blockchain/getTxReceipt";
import { getUserOpReceipt } from "./transaction/blockchain/getUserOpReceipt";
import { sendSignedTransaction } from "./transaction/blockchain/sendSignedTx";
import { sendSignedUserOp } from "./transaction/blockchain/sendSignedUserOp";
import { checkGroupStatus } from "./transaction/group";
import { sendSignedTransaction } from "./transaction/sendSignedTx";
import { sendSignedUserOp } from "./transaction/sendSignedUserOp";

export const withRoutes = async (fastify: FastifyInstance) => {
// Backend Wallets
Expand Down Expand Up @@ -215,6 +217,8 @@ export const withRoutes = async (fastify: FastifyInstance) => {
await fastify.register(cancelTransaction);
await fastify.register(sendSignedTransaction);
await fastify.register(sendSignedUserOp);
await fastify.register(getTxHashReceipt);
await fastify.register(getUserOpReceipt);

// Extensions
await fastify.register(accountFactoryRoutes);
Expand Down
147 changes: 147 additions & 0 deletions src/server/routes/transaction/blockchain/getTxReceipt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { Static, Type } from "@sinclair/typebox";
import { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import { getSdk } from "../../../../utils/cache/getSdk";
import { standardResponseSchema } from "../../../schemas/sharedApiSchemas";
import { getChainIdFromChain } from "../../../utils/chain";

// INPUT
const requestSchema = Type.Object({
txHash: Type.String({
description: "Transaction hash",
examples: [
"0xd9bcba8f5bc4ce5bf4d631b2a0144329c1df3b56ddb9fc64637ed3a4219dd087",
],
pattern: "^0x([A-Fa-f0-9]{64})$",
}),
chain: Type.String({
examples: ["mumbai"],
description: "Chain ID or name",
}),
});

// OUTPUT
export const responseBodySchema = Type.Object({
result: Type.Union([
Type.Partial(
Type.Object({
to: Type.String(),
from: Type.String(),
contractAddress: Type.Union([Type.String(), Type.Null()]),
transactionIndex: Type.Number(),
root: Type.String(),
gasUsed: Type.String(),
logsBloom: Type.String(),
blockHash: Type.String(),
transactionHash: Type.String(),
logs: Type.Array(Type.Any()),
blockNumber: Type.Number(),
confirmations: Type.Number(),
cumulativeGasUsed: Type.String(),
effectiveGasPrice: Type.String(),
byzantium: Type.Boolean(),
type: Type.Number(),
status: Type.Number(),
}),
),
Type.Null(),
]),
});

responseBodySchema.example = {
result: {
to: "0xd7419703c2D5737646525A8660906eCb612875BD",
from: "0x9783Eb2a93A58b24CFeC56F94b30aB6e29fF4b38",
contractAddress: null,
transactionIndex: 69,
gasUsed: "21000",
logsBloom:
"0x00000000000200000000000000000000000000000000000000000000000000000000000000000000000000100002000000008000000000000000000000002000000000000000000000000000000000800000000000000000000100000000040000000000000000000000000000000000008000000000000080000000000000000000000000000000000000000000000000000000000000040000000000000000200000000000004000800000000000000000000000000000000000000000004000000000000000000001000000000000000000000000800000108000000000000000000000000000000000000000000000000000000000000000008000100000",
blockHash:
"0x9be85de9e6a0717ed2e7c9035f7bd748a4b20bc9d6e04a6875fa69311421d971",
transactionHash:
"0xd9bcba8f5bc4ce5bf4d631b2a0144329c1df3b56ddb9fc64637ed3a4219dd087",
logs: [
{
transactionIndex: 69,
blockNumber: 51048531,
transactionHash:
"0xd9bcba8f5bc4ce5bf4d631b2a0144329c1df3b56ddb9fc64637ed3a4219dd087",
address: "0x0000000000000000000000000000000000001010",
topics: [
"0xe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c4",
"0x0000000000000000000000000000000000000000000000000000000000001010",
"0x0000000000000000000000009783eb2a93a58b24cfec56f94b30ab6e29ff4b38",
"0x000000000000000000000000d7419703c2d5737646525a8660906ecb612875bd",
],
data: "0x00000000000000000000000000000000000000000000000006a4d6a25acff49800000000000000000000000000000000000000000000000006b787e1bb9f06f80000000000000000000000000000000000000000000000051ac78aecb02246820000000000000000000000000000000000000000000000000012b13f60cf1260000000000000000000000000000000000000000000000005216c618f0af23b1a",
logIndex: 181,
blockHash:
"0x9be85de9e6a0717ed2e7c9035f7bd748a4b20bc9d6e04a6875fa69311421d971",
},
{
transactionIndex: 69,
blockNumber: 51048531,
transactionHash:
"0xd9bcba8f5bc4ce5bf4d631b2a0144329c1df3b56ddb9fc64637ed3a4219dd087",
address: "0x0000000000000000000000000000000000001010",
topics: [
"0x4dfe1bbbcf077ddc3e01291eea2d5c70c2b422b415d95645b9adcfd678cb1d63",
"0x0000000000000000000000000000000000000000000000000000000000001010",
"0x0000000000000000000000009783eb2a93a58b24cfec56f94b30ab6e29ff4b38",
"0x000000000000000000000000a8b52f02108aa5f4b675bdcc973760022d7c6020",
],
data: "0x00000000000000000000000000000000000000000000000000046a2c9f9fd11800000000000000000000000000000000000000000000000006ce8dc1a70476680000000000000000000000000000000000000000000006df390338516c1391c300000000000000000000000000000000000000000000000006ca23950764a5500000000000000000000000000000000000000000000006df3907a27e0bb362db",
logIndex: 182,
blockHash:
"0x9be85de9e6a0717ed2e7c9035f7bd748a4b20bc9d6e04a6875fa69311421d971",
},
],
blockNumber: 51048531,
confirmations: 3232643,
cumulativeGasUsed: "5751865",
effectiveGasPrice: "257158085297",
status: 1,
type: 2,
byzantium: true,
},
};

export async function getTxHashReceipt(fastify: FastifyInstance) {
fastify.route<{
Params: Static<typeof requestSchema>;
Reply: Static<typeof responseBodySchema>;
}>({
method: "GET",
url: "/transaction/:chain/tx-hash/:txHash",
schema: {
summary: "Get transaction receipt from transaction hash",
description: "Get the transaction receipt from a transaction hash.",
tags: ["Transaction"],
operationId: "txHashReceipt",
params: requestSchema,
response: {
...standardResponseSchema,
[StatusCodes.OK]: responseBodySchema,
},
},
handler: async (request, reply) => {
const { chain, txHash } = request.params;

const chainId = await getChainIdFromChain(chain);
const sdk = await getSdk({ chainId });
const receipt = await sdk.getProvider().getTransactionReceipt(txHash);

reply.status(StatusCodes.OK).send({
result: receipt
? {
...receipt,
gasUsed: receipt.gasUsed.toString(),
cumulativeGasUsed: receipt.cumulativeGasUsed.toString(),
effectiveGasPrice: receipt.effectiveGasPrice.toString(),
}
: null,
});
},
});
}
150 changes: 150 additions & 0 deletions src/server/routes/transaction/blockchain/getUserOpReceipt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { Static, Type } from "@sinclair/typebox";
import { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import { env } from "../../../../utils/env";
import { createCustomError } from "../../../middleware/error";
import { standardResponseSchema } from "../../../schemas/sharedApiSchemas";
import { getChainIdFromChain } from "../../../utils/chain";

// INPUT
const requestSchema = Type.Object({
userOpHash: Type.String({
description: "User operation hash",
examples: [
"0xa5a579c6fd86c2d8a4d27f5bb22796614d3a31bbccaba8f3019ec001e001b95f",
],
pattern: "^0x([A-Fa-f0-9]{64})$",
}),
chain: Type.String({
examples: ["mumbai"],
description: "Chain ID or name",
}),
});

// OUTPUT
export const responseBodySchema = Type.Object({
result: Type.Union([
Type.Partial(
Type.Object({
userOpHash: Type.String(),
sender: Type.String(),
nonce: Type.String(),
actualGasCost: Type.String(),
actualGasUsed: Type.String(),
success: Type.Boolean(),
paymaster: Type.String(),
logs: Type.Array(Type.Any()),
receipt: Type.Partial(
Type.Object({
type: Type.String(),
status: Type.String(),
cumulativeGasUsed: Type.String(),
logsBloom: Type.String(),
logs: Type.Array(Type.Any()),
transactionHash: Type.String(),
from: Type.String(),
to: Type.String(),
contractAddress: Type.Union([Type.String(), Type.Null()]),
gasUsed: Type.String(),
effectiveGasPrice: Type.String(),
blockHash: Type.String(),
blockNumber: Type.String(),
transactionIndex: Type.Number(),
blobGasUsed: Type.String(),
}),
),
}),
),
Type.Null(),
]),
});

responseBodySchema.example = {
result: {
userOpHash:
"0xa5a579c6fd86c2d8a4d27f5bb22796614d3a31bbccaba8f3019ec001e001b95f",
sender: "0x8C6bdb488F664EB98E12cB2671fE2389Cc227D33",
nonce: "0x18554d9a95404c5e8ac591f8608a18f80000000000000000",
actualGasCost: "0x4b3b147f788710",
actualGasUsed: "0x7f550",
success: true,
paymaster: "0xe3dc822D77f8cA7ac74c30B0dfFEA9FcDCAAA321",
logs: [],
receipt: {
type: "eip1559",
status: "success",
cumulativeGasUsed: "0x4724c3",
logsBloom:
"0x010004000800020000000040000000000000040000000000000010000004000000080000001000000212841100000000041080000000000020000240000000000800000022001000400000080000028000040000000000200001000010000000000000000a0000000000000000800800000000004110004080800110282000000000000002000000000000000000000000000200000400000000000000240040200002000000000000400000000002000140000000000000000002200000004000000002000000000021000000000000000000000000800080108020000020000000080000000000000000000000000000000000000000000108000000102000",
logs: [],
transactionHash:
"0x57465d20d634421008a167cfcfcde94847dba9d6b5d3652b071d4b84e5ce74ff",
from: "0x43370996a3aff7b66b3ac7676dd973d01ecec039",
to: "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789",
contractAddress: null,
gasUsed: "0x7ff5a",
effectiveGasPrice: "0x89b098f46",
blockHash:
"0xeaeec1eff4095bdcae44d86574cf1bf08b14b26be571b7c2290f32f9f250c103",
blockNumber: "0x31de70e",
transactionIndex: 32,
blobGasUsed: "0x0",
},
},
};

export async function getUserOpReceipt(fastify: FastifyInstance) {
fastify.route<{
Params: Static<typeof requestSchema>;
Reply: Static<typeof responseBodySchema>;
}>({
method: "GET",
url: "/transaction/:chain/userop-hash/:userOpHash",
schema: {
summary: "Get transaction receipt from user-op hash",
description: "Get the transaction receipt from a user-op hash.",
tags: ["Transaction"],
operationId: "useropHashReceipt",
params: requestSchema,
response: {
...standardResponseSchema,
[StatusCodes.OK]: responseBodySchema,
},
},
handler: async (request, reply) => {
const { chain, userOpHash } = request.params;
const chainId = await getChainIdFromChain(chain);

try {
const url = `https://${chainId}.bundler.thirdweb.com`;
const resp = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-secret-key": env.THIRDWEB_API_SECRET_KEY,
},
body: JSON.stringify({
id: 0,
jsonrpc: "2.0",
method: "eth_getUserOperationReceipt",
params: [userOpHash],
}),
});
if (!resp.ok) {
throw `Unexpected status ${resp.status} - ${await resp.text()}`;
}

const json = await resp.json();
reply.status(StatusCodes.OK).send({
result: json.result,
});
} catch (e) {
throw createCustomError(
"Unable to get receipt.",
StatusCodes.INTERNAL_SERVER_ERROR,
"UserOpReceiptError",
);
}
},
});
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Static, Type } from "@sinclair/typebox";
import { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import { getSdk } from "../../../utils/cache/getSdk";
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";
import { getChainIdFromChain } from "../../utils/chain";
import { getSdk } from "../../../../utils/cache/getSdk";
import { standardResponseSchema } from "../../../schemas/sharedApiSchemas";
import { getChainIdFromChain } from "../../../utils/chain";

const ParamsSchema = Type.Object({
chain: Type.String(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { Static, Type } from "@sinclair/typebox";
import { Value } from "@sinclair/typebox/value";
import { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import { thirdwebClientId } from "../../../utils/api-keys";
import { env } from "../../../utils/env";
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";
import { getChainIdFromChain } from "../../utils/chain";
import { thirdwebClientId } from "../../../../utils/api-keys";
import { env } from "../../../../utils/env";
import { standardResponseSchema } from "../../../schemas/sharedApiSchemas";
import { getChainIdFromChain } from "../../../utils/chain";

const UserOp = Type.Object({
sender: Type.String(),
Expand Down

0 comments on commit a2c7f8f

Please sign in to comment.