Skip to content

Commit

Permalink
Add send raw transaction & user operation endpoints (#350)
Browse files Browse the repository at this point in the history
* Add send raw transaction & user operation endpoints

* Reset

* Update

* Update

* Update
  • Loading branch information
adam-maj authored Dec 13, 2023
1 parent 866f8bb commit 6a92699
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 7 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"@fastify/websocket": "^8.2.0",
"@google-cloud/kms": "^4.0.0",
"@prisma/client": "5.2.0",
"@sinclair/typebox": "^0.28",
"@sinclair/typebox": "^0.31.28",
"@t3-oss/env-core": "^0.6.0",
"@thirdweb-dev/auth": "^4.1.0-nightly-c238fde8-20231020022304",
"@thirdweb-dev/chains": "^0.1.61-nightly-d2001ca4-20231209002129",
Expand Down
4 changes: 2 additions & 2 deletions src/server/routes/backend-wallet/sendTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export async function sendTransaction(fastify: FastifyInstance) {
method: "POST",
url: "/backend-wallet/:chain/send-transaction",
schema: {
summary: "Send a raw transaction",
description: "Send a raw transaction with transaction parameters",
summary: "Send a transaction",
description: "Send a transaction with transaction parameters",
tags: ["Backend Wallet"],
operationId: "sendTransaction",
params: ParamsSchema,
Expand Down
4 changes: 4 additions & 0 deletions src/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ import { healthCheck } from "./health";
import { home } from "./home";
import { updateRelayer } from "./relayer/update";
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 @@ -192,6 +194,8 @@ export const withRoutes = async (fastify: FastifyInstance) => {
await fastify.register(checkGroupStatus);
await fastify.register(retryTransaction);
await fastify.register(cancelTransaction);
await fastify.register(sendSignedTransaction);
await fastify.register(sendSignedUserOp);

// Extensions
await fastify.register(accountFactoryRoutes);
Expand Down
57 changes: 57 additions & 0 deletions src/server/routes/transaction/sendSignedTx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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";

const ParamsSchema = Type.Object({
chain: Type.String(),
});

const BodySchema = Type.Object({
signedTransaction: Type.String(),
});

const ReplySchema = Type.Object({
result: Type.Object({
transactionHash: Type.String(),
}),
});

export async function sendSignedTransaction(fastify: FastifyInstance) {
fastify.route<{
Params: Static<typeof ParamsSchema>;
Body: Static<typeof BodySchema>;
Reply: Static<typeof ReplySchema>;
}>({
method: "POST",
url: "/transaction/:chain/send-signed-transaction",
schema: {
summary: "Send a signed transaction",
description: "Send a signed transaction",
tags: ["Transaction"],
operationId: "sendRawTransaction",
params: ParamsSchema,
body: BodySchema,
response: {
...standardResponseSchema,
[StatusCodes.OK]: ReplySchema,
},
},
handler: async (req, res) => {
const { chain } = req.params;
const { signedTransaction } = req.body;
const chainId = await getChainIdFromChain(chain);
const sdk = await getSdk({ chainId });

const txRes = await sdk.getProvider().sendTransaction(signedTransaction);

res.status(200).send({
result: {
transactionHash: txRes.hash,
},
});
},
});
}
136 changes: 136 additions & 0 deletions src/server/routes/transaction/sendSignedUserOp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { Static, Type } from "@sinclair/typebox";
import { Value } from "@sinclair/typebox/value";
import { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import { deriveClientId } 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(),
nonce: Type.String(),
initCode: Type.String(),
callData: Type.String(),
callGasLimit: Type.String(),
verificationGasLimit: Type.String(),
preVerificationGas: Type.String(),
maxFeePerGas: Type.String(),
maxPriorityFeePerGas: Type.String(),
paymasterAndData: Type.String(),
signature: Type.String(),
});

const UserOpString = Type.Transform(Type.String())
.Decode((signedUserOp) => JSON.parse(signedUserOp) as Static<typeof UserOp>)
.Encode((userOp) => JSON.stringify(userOp));

const ParamsSchema = Type.Object({
chain: Type.String(),
});

const BodySchema = Type.Object({
// signedUserOp: Type.Union([UserOpString, UserOp]),
signedUserOp: Type.Any(),
});

const ReplySchema = Type.Union([
Type.Object({
result: Type.Object({
userOpHash: Type.String(),
}),
}),
Type.Object({
error: Type.Object({
message: Type.String(),
}),
}),
]);

type RpcResponse =
| {
result: string;
error: undefined;
}
| {
result: undefined;
error: {
message: string;
};
};

export async function sendSignedUserOp(fastify: FastifyInstance) {
fastify.route<{
Params: Static<typeof ParamsSchema>;
Body: Static<typeof BodySchema>;
Reply: Static<typeof ReplySchema>;
}>({
method: "POST",
url: "/transaction/:chain/send-signed-user-op",
schema: {
summary: "Send a signed user operation",
description: "Send a signed user operation",
tags: ["Transaction"],
operationId: "sendSignedUserOp",
params: ParamsSchema,
body: BodySchema,
response: {
...standardResponseSchema,
[StatusCodes.OK]: ReplySchema,
},
},
handler: async (req, res) => {
const { chain } = req.params;
const { signedUserOp } = req.body;
const chainId = await getChainIdFromChain(chain);

let userOp: Static<typeof UserOp>;
if (typeof signedUserOp === "string") {
try {
userOp = Value.Decode(UserOpString, signedUserOp);
} catch (err: any) {
return res.status(400).send({
error: {
message: `Invalid signed user operation. - ${err.message || err}`,
},
});
}
} else {
userOp = signedUserOp;
}

const entryPointAddress = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789";
const userOpRes = await fetch(`https://${chainId}.bundler.thirdweb.com`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-client-id": deriveClientId(env.THIRDWEB_API_SECRET_KEY),
"x-secret-key": env.THIRDWEB_API_SECRET_KEY,
},
body: JSON.stringify({
id: 1,
jsonrpc: "2.0",
method: "eth_sendUserOperation",
params: [userOp, entryPointAddress],
}),
});

const { result: userOpHash, error } =
(await userOpRes.json()) as RpcResponse;

if (error) {
return res.status(400).send({
error: {
message: `Failed to send - ${error.message || error}`,
},
});
}

return res.status(200).send({
result: {
userOpHash,
},
});
},
});
}
7 changes: 7 additions & 0 deletions src/utils/api-keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { sha256HexSync } from "@thirdweb-dev/crypto";

export const deriveClientId = (secretKey: string): string => {
const hashedSecretKey = sha256HexSync(secretKey);
const derivedClientId = hashedSecretKey.slice(0, 32);
return derivedClientId;
};
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2286,10 +2286,10 @@
"@sentry/types" "5.30.0"
tslib "^1.9.3"

"@sinclair/typebox@^0.28":
version "0.28.11"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.28.11.tgz#f094caefc315e1a9b4e649056f661ddb20fcea97"
integrity sha512-8QPhkOowccAdXa/ra54pq+UVYvzbKjYMuojxCOTFq+yyEfcWZJSdlIVdivTRrIq7Mgjx1n4E37t8Js/RXwyvUg==
"@sinclair/typebox@^0.31.28":
version "0.31.28"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.31.28.tgz#b68831e7bc7d09daac26968ea32f42bedc968ede"
integrity sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==

"@smithy/abort-controller@^2.0.5":
version "2.0.5"
Expand Down

0 comments on commit 6a92699

Please sign in to comment.