Skip to content

Commit 6a92699

Browse files
authored
Add send raw transaction & user operation endpoints (#350)
* Add send raw transaction & user operation endpoints * Reset * Update * Update * Update
1 parent 866f8bb commit 6a92699

File tree

7 files changed

+211
-7
lines changed

7 files changed

+211
-7
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"@fastify/websocket": "^8.2.0",
4141
"@google-cloud/kms": "^4.0.0",
4242
"@prisma/client": "5.2.0",
43-
"@sinclair/typebox": "^0.28",
43+
"@sinclair/typebox": "^0.31.28",
4444
"@t3-oss/env-core": "^0.6.0",
4545
"@thirdweb-dev/auth": "^4.1.0-nightly-c238fde8-20231020022304",
4646
"@thirdweb-dev/chains": "^0.1.61-nightly-d2001ca4-20231209002129",

src/server/routes/backend-wallet/sendTransaction.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ export async function sendTransaction(fastify: FastifyInstance) {
4444
method: "POST",
4545
url: "/backend-wallet/:chain/send-transaction",
4646
schema: {
47-
summary: "Send a raw transaction",
48-
description: "Send a raw transaction with transaction parameters",
47+
summary: "Send a transaction",
48+
description: "Send a transaction with transaction parameters",
4949
tags: ["Backend Wallet"],
5050
operationId: "sendTransaction",
5151
params: ParamsSchema,

src/server/routes/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ import { healthCheck } from "./health";
100100
import { home } from "./home";
101101
import { updateRelayer } from "./relayer/update";
102102
import { checkGroupStatus } from "./transaction/group";
103+
import { sendSignedTransaction } from "./transaction/sendSignedTx";
104+
import { sendSignedUserOp } from "./transaction/sendSignedUserOp";
103105

104106
export const withRoutes = async (fastify: FastifyInstance) => {
105107
// Backend Wallets
@@ -192,6 +194,8 @@ export const withRoutes = async (fastify: FastifyInstance) => {
192194
await fastify.register(checkGroupStatus);
193195
await fastify.register(retryTransaction);
194196
await fastify.register(cancelTransaction);
197+
await fastify.register(sendSignedTransaction);
198+
await fastify.register(sendSignedUserOp);
195199

196200
// Extensions
197201
await fastify.register(accountFactoryRoutes);
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
const ParamsSchema = Type.Object({
9+
chain: Type.String(),
10+
});
11+
12+
const BodySchema = Type.Object({
13+
signedTransaction: Type.String(),
14+
});
15+
16+
const ReplySchema = Type.Object({
17+
result: Type.Object({
18+
transactionHash: Type.String(),
19+
}),
20+
});
21+
22+
export async function sendSignedTransaction(fastify: FastifyInstance) {
23+
fastify.route<{
24+
Params: Static<typeof ParamsSchema>;
25+
Body: Static<typeof BodySchema>;
26+
Reply: Static<typeof ReplySchema>;
27+
}>({
28+
method: "POST",
29+
url: "/transaction/:chain/send-signed-transaction",
30+
schema: {
31+
summary: "Send a signed transaction",
32+
description: "Send a signed transaction",
33+
tags: ["Transaction"],
34+
operationId: "sendRawTransaction",
35+
params: ParamsSchema,
36+
body: BodySchema,
37+
response: {
38+
...standardResponseSchema,
39+
[StatusCodes.OK]: ReplySchema,
40+
},
41+
},
42+
handler: async (req, res) => {
43+
const { chain } = req.params;
44+
const { signedTransaction } = req.body;
45+
const chainId = await getChainIdFromChain(chain);
46+
const sdk = await getSdk({ chainId });
47+
48+
const txRes = await sdk.getProvider().sendTransaction(signedTransaction);
49+
50+
res.status(200).send({
51+
result: {
52+
transactionHash: txRes.hash,
53+
},
54+
});
55+
},
56+
});
57+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { Static, Type } from "@sinclair/typebox";
2+
import { Value } from "@sinclair/typebox/value";
3+
import { FastifyInstance } from "fastify";
4+
import { StatusCodes } from "http-status-codes";
5+
import { deriveClientId } from "../../../utils/api-keys";
6+
import { env } from "../../../utils/env";
7+
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";
8+
import { getChainIdFromChain } from "../../utils/chain";
9+
10+
const UserOp = Type.Object({
11+
sender: Type.String(),
12+
nonce: Type.String(),
13+
initCode: Type.String(),
14+
callData: Type.String(),
15+
callGasLimit: Type.String(),
16+
verificationGasLimit: Type.String(),
17+
preVerificationGas: Type.String(),
18+
maxFeePerGas: Type.String(),
19+
maxPriorityFeePerGas: Type.String(),
20+
paymasterAndData: Type.String(),
21+
signature: Type.String(),
22+
});
23+
24+
const UserOpString = Type.Transform(Type.String())
25+
.Decode((signedUserOp) => JSON.parse(signedUserOp) as Static<typeof UserOp>)
26+
.Encode((userOp) => JSON.stringify(userOp));
27+
28+
const ParamsSchema = Type.Object({
29+
chain: Type.String(),
30+
});
31+
32+
const BodySchema = Type.Object({
33+
// signedUserOp: Type.Union([UserOpString, UserOp]),
34+
signedUserOp: Type.Any(),
35+
});
36+
37+
const ReplySchema = Type.Union([
38+
Type.Object({
39+
result: Type.Object({
40+
userOpHash: Type.String(),
41+
}),
42+
}),
43+
Type.Object({
44+
error: Type.Object({
45+
message: Type.String(),
46+
}),
47+
}),
48+
]);
49+
50+
type RpcResponse =
51+
| {
52+
result: string;
53+
error: undefined;
54+
}
55+
| {
56+
result: undefined;
57+
error: {
58+
message: string;
59+
};
60+
};
61+
62+
export async function sendSignedUserOp(fastify: FastifyInstance) {
63+
fastify.route<{
64+
Params: Static<typeof ParamsSchema>;
65+
Body: Static<typeof BodySchema>;
66+
Reply: Static<typeof ReplySchema>;
67+
}>({
68+
method: "POST",
69+
url: "/transaction/:chain/send-signed-user-op",
70+
schema: {
71+
summary: "Send a signed user operation",
72+
description: "Send a signed user operation",
73+
tags: ["Transaction"],
74+
operationId: "sendSignedUserOp",
75+
params: ParamsSchema,
76+
body: BodySchema,
77+
response: {
78+
...standardResponseSchema,
79+
[StatusCodes.OK]: ReplySchema,
80+
},
81+
},
82+
handler: async (req, res) => {
83+
const { chain } = req.params;
84+
const { signedUserOp } = req.body;
85+
const chainId = await getChainIdFromChain(chain);
86+
87+
let userOp: Static<typeof UserOp>;
88+
if (typeof signedUserOp === "string") {
89+
try {
90+
userOp = Value.Decode(UserOpString, signedUserOp);
91+
} catch (err: any) {
92+
return res.status(400).send({
93+
error: {
94+
message: `Invalid signed user operation. - ${err.message || err}`,
95+
},
96+
});
97+
}
98+
} else {
99+
userOp = signedUserOp;
100+
}
101+
102+
const entryPointAddress = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789";
103+
const userOpRes = await fetch(`https://${chainId}.bundler.thirdweb.com`, {
104+
method: "POST",
105+
headers: {
106+
"Content-Type": "application/json",
107+
"x-client-id": deriveClientId(env.THIRDWEB_API_SECRET_KEY),
108+
"x-secret-key": env.THIRDWEB_API_SECRET_KEY,
109+
},
110+
body: JSON.stringify({
111+
id: 1,
112+
jsonrpc: "2.0",
113+
method: "eth_sendUserOperation",
114+
params: [userOp, entryPointAddress],
115+
}),
116+
});
117+
118+
const { result: userOpHash, error } =
119+
(await userOpRes.json()) as RpcResponse;
120+
121+
if (error) {
122+
return res.status(400).send({
123+
error: {
124+
message: `Failed to send - ${error.message || error}`,
125+
},
126+
});
127+
}
128+
129+
return res.status(200).send({
130+
result: {
131+
userOpHash,
132+
},
133+
});
134+
},
135+
});
136+
}

src/utils/api-keys.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { sha256HexSync } from "@thirdweb-dev/crypto";
2+
3+
export const deriveClientId = (secretKey: string): string => {
4+
const hashedSecretKey = sha256HexSync(secretKey);
5+
const derivedClientId = hashedSecretKey.slice(0, 32);
6+
return derivedClientId;
7+
};

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2286,10 +2286,10 @@
22862286
"@sentry/types" "5.30.0"
22872287
tslib "^1.9.3"
22882288

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

22942294
"@smithy/abort-controller@^2.0.5":
22952295
version "2.0.5"

0 commit comments

Comments
 (0)