Skip to content

Commit c73c18d

Browse files
authored
chore: Update signTransaction to v5 SDK (#718)
* fix: Return 400 on invalid wallet * fix: Update signTransaction route to use v5 SDK * fix test * remove try catch
1 parent 873c0ef commit c73c18d

File tree

9 files changed

+204
-23
lines changed

9 files changed

+204
-23
lines changed

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

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1-
import { Static, Type } from "@sinclair/typebox";
2-
import { FastifyInstance } from "fastify";
1+
import { Type, type Static } from "@sinclair/typebox";
2+
import type { FastifyInstance } from "fastify";
33
import { StatusCodes } from "http-status-codes";
4-
import { getWallet } from "../../../utils/cache/getWallet";
4+
import type { Hex } from "thirdweb";
5+
import { getAccount } from "../../../utils/account";
6+
import {
7+
getChecksumAddress,
8+
maybeBigInt,
9+
maybeInt,
10+
} from "../../../utils/primitiveTypes";
11+
import { toTransactionType } from "../../../utils/sdk";
12+
import { createCustomError } from "../../middleware/error";
513
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";
614
import { walletHeaderSchema } from "../../schemas/wallet";
715

816
const requestBodySchema = Type.Object({
917
transaction: Type.Object({
1018
to: Type.Optional(Type.String()),
11-
from: Type.Optional(Type.String()),
1219
nonce: Type.Optional(Type.String()),
1320
gasLimit: Type.Optional(Type.String()),
1421
gasPrice: Type.Optional(Type.String()),
@@ -19,7 +26,6 @@ const requestBodySchema = Type.Object({
1926
accessList: Type.Optional(Type.Any()),
2027
maxFeePerGas: Type.Optional(Type.String()),
2128
maxPriorityFeePerGas: Type.Optional(Type.String()),
22-
customData: Type.Optional(Type.Record(Type.String(), Type.Any())),
2329
ccipReadEnabled: Type.Optional(Type.Boolean()),
2430
}),
2531
});
@@ -52,16 +58,39 @@ export async function signTransaction(fastify: FastifyInstance) {
5258
const { "x-backend-wallet-address": walletAddress } =
5359
request.headers as Static<typeof walletHeaderSchema>;
5460

55-
const wallet = await getWallet({
61+
const account = await getAccount({
5662
chainId: 1,
57-
walletAddress,
63+
from: getChecksumAddress(walletAddress),
5864
});
65+
if (!account.signTransaction) {
66+
throw createCustomError(
67+
'This backend wallet does not support "signTransaction".',
68+
StatusCodes.BAD_REQUEST,
69+
"SIGN_TRANSACTION_UNIMPLEMENTED",
70+
);
71+
}
5972

60-
const signer = await wallet.getSigner();
61-
const signedMessage = await signer.signTransaction(transaction);
73+
// @TODO: Assert type to viem TransactionSerializable.
74+
const serializableTransaction: any = {
75+
chainId: transaction.chainId,
76+
to: getChecksumAddress(transaction.to),
77+
nonce: maybeInt(transaction.nonce),
78+
gas: maybeBigInt(transaction.gasLimit),
79+
gasPrice: maybeBigInt(transaction.gasPrice),
80+
data: transaction.data as Hex | undefined,
81+
value: maybeBigInt(transaction.value),
82+
type: transaction.type
83+
? toTransactionType(transaction.type)
84+
: undefined,
85+
accessList: transaction.accessList,
86+
maxFeePerGas: maybeBigInt(transaction.maxFeePerGas),
87+
maxPriorityFeePerGas: maybeBigInt(transaction.maxPriorityFeePerGas),
88+
ccipReadEnabled: transaction.ccipReadEnabled,
89+
};
90+
const signature = await account.signTransaction(serializableTransaction);
6291

6392
reply.status(StatusCodes.OK).send({
64-
result: signedMessage,
93+
result: signature,
6594
});
6695
},
6796
});

src/server/routes/transaction/blockchain/getReceipt.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import { stringify } from "thirdweb/utils";
1111
import type { TransactionReceipt } from "viem";
1212
import { getChain } from "../../../../utils/chain";
1313
import {
14+
fromTransactionStatus,
15+
fromTransactionType,
1416
thirdwebClient,
15-
toTransactionStatus,
16-
toTransactionType,
1717
} from "../../../../utils/sdk";
1818
import { createCustomError } from "../../../middleware/error";
1919
import { AddressSchema, TransactionHashSchema } from "../../../schemas/address";
@@ -166,8 +166,8 @@ export async function getTransactionReceipt(fastify: FastifyInstance) {
166166
cumulativeGasUsed: toHex(receipt.cumulativeGasUsed),
167167
effectiveGasPrice: toHex(receipt.effectiveGasPrice),
168168
blockNumber: Number(receipt.blockNumber),
169-
type: toTransactionType(receipt.type),
170-
status: toTransactionStatus(receipt.status),
169+
type: fromTransactionType(receipt.type),
170+
status: fromTransactionStatus(receipt.status),
171171
},
172172
});
173173
},

src/server/schemas/sharedApiSchemas.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { Static, Type } from "@sinclair/typebox";
1+
import { Type, type Static } from "@sinclair/typebox";
22
import { PREBUILT_CONTRACTS_MAP } from "@thirdweb-dev/sdk";
3-
import { RouteGenericInterface } from "fastify";
4-
import { FastifySchema } from "fastify/types/schema";
3+
import type { RouteGenericInterface } from "fastify";
4+
import type { FastifySchema } from "fastify/types/schema";
55
import { StatusCodes } from "http-status-codes";
66
import { AddressSchema } from "./address";
77

@@ -182,7 +182,6 @@ export const erc20ContractParamSchema = Type.Object({
182182
}),
183183
contractAddress: {
184184
...AddressSchema,
185-
examples: ["0x365b83D67D5539C6583b9c0266A548926Bf216F4"],
186185
description: "ERC20 contract address",
187186
},
188187
});

src/server/schemas/wallet/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const walletWithAAHeaderSchema = Type.Object({
2525
"x-account-factory-address": Type.Optional({
2626
...AddressSchema,
2727
description:
28-
"Smart account factory address. If omitted, engine will try to resolve it from the chain.",
28+
"Smart account factory address. If omitted, Engine will try to resolve it from the contract.",
2929
}),
3030
"x-account-salt": Type.Optional(
3131
Type.String({

src/utils/primitiveTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { Address } from "thirdweb";
22
import { checksumAddress } from "thirdweb/utils";
33

44
export const maybeBigInt = (val?: string) => (val ? BigInt(val) : undefined);
5+
export const maybeInt = (val?: string) =>
6+
val ? Number.parseInt(val) : undefined;
57

68
// These overloads hint TS at the response type (ex: Address if `val` is Address).
79
export function normalizeAddress(val: Address): Address;

src/utils/sdk.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { sha256HexSync } from "@thirdweb-dev/crypto";
22
import { createThirdwebClient } from "thirdweb";
3-
import { TransactionReceipt } from "thirdweb/dist/types/transaction/types";
3+
import type { TransactionType } from "viem";
44
import { env } from "./env";
55

66
export const thirdwebClientId = sha256HexSync(
@@ -18,14 +18,23 @@ export const thirdwebClient = createThirdwebClient({
1818
* Helper functions to handle v4 -> v5 SDK migration.
1919
*/
2020

21-
export const toTransactionStatus = (status: "success" | "reverted"): number =>
21+
export const fromTransactionStatus = (status: "success" | "reverted") =>
2222
status === "success" ? 1 : 0;
2323

24-
export const toTransactionType = (type: TransactionReceipt["type"]): number => {
24+
export const fromTransactionType = (type: TransactionType) => {
2525
if (type === "legacy") return 0;
2626
if (type === "eip1559") return 1;
2727
if (type === "eip2930") return 2;
2828
if (type === "eip4844") return 3;
2929
if (type === "eip7702") return 4;
3030
throw new Error(`Unexpected transaction type ${type}`);
3131
};
32+
33+
export const toTransactionType = (value: number) => {
34+
if (value === 0) return "legacy";
35+
if (value === 1) return "eip1559";
36+
if (value === 2) return "eip2930";
37+
if (value === 3) return "eip4844";
38+
if (value === 4) return "eip7702";
39+
throw new Error(`Unexpected transaction type number ${value}`);
40+
};

test/e2e/.env.test.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
THIRDWEB_API_SECRET_KEY=""
12
ENGINE_ACCESS_TOKEN=""
2-
ENGINE_URL=""
3+
ENGINE_URL="http://127.0.0.1:3005"
34
ANVIL_URL=""

test/e2e/chain.test.ts

Whitespace-only changes.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { setup } from "./setup";
3+
4+
describe("signTransaction route", () => {
5+
test("Sign a legacy transaction", async () => {
6+
const { engine, backendWallet } = await setup();
7+
8+
const res = await engine.backendWallet.signTransaction(backendWallet, {
9+
transaction: {
10+
type: 0,
11+
chainId: 1,
12+
to: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4",
13+
nonce: "42",
14+
gasLimit: "88000",
15+
gasPrice: "2000000000",
16+
value: "100000000000000000",
17+
},
18+
});
19+
20+
expect(res.result).toEqual(
21+
"0xf86c2a8477359400830157c094152e208d08cd3ea1aa5d179b2e3eba7d1a733ef488016345785d8a00008026a05da3d31d9cfbb4026b6e187c81952199d567e182d9c2ecc72acf98e4e6ce4875a03b2815b79881092ab5a4f74e6725081d652becad8495b815c14abb56cc782041",
22+
);
23+
});
24+
25+
test("Sign an eip-1559 transaction", async () => {
26+
const { engine, backendWallet } = await setup();
27+
28+
const res = await engine.backendWallet.signTransaction(backendWallet, {
29+
transaction: {
30+
type: 1,
31+
chainId: 137,
32+
to: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4",
33+
nonce: "42",
34+
gasLimit: "88000",
35+
maxFeePerGas: "2000000000",
36+
maxPriorityFeePerGas: "200000000",
37+
value: "100000000000000000",
38+
accessList: [
39+
{
40+
address: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4",
41+
storageKeys: [
42+
"0x0000000000000000000000000000000000000000000000000000000000000001",
43+
],
44+
},
45+
],
46+
},
47+
});
48+
49+
expect(res.result).toEqual(
50+
"0x02f8ad81892a840bebc2008477359400830157c094152e208d08cd3ea1aa5d179b2e3eba7d1a733ef488016345785d8a000080f838f794152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4e1a0000000000000000000000000000000000000000000000000000000000000000180a050fd32589ec2e2b49b3bce79d9420474115490ea0693ada75a62d003c6ada1aaa06fbc93d08a7604fbca5c31af92a662ff6be3b5a9f75214b7cd5db5feab2fc444",
51+
);
52+
});
53+
54+
test("Sign an eip-2930 transaction", async () => {
55+
const { engine, backendWallet } = await setup();
56+
57+
const res = await engine.backendWallet.signTransaction(backendWallet, {
58+
transaction: {
59+
type: 2,
60+
chainId: 137,
61+
to: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4",
62+
nonce: "42",
63+
gasLimit: "88000",
64+
value: "100000000000000000",
65+
accessList: [
66+
{
67+
address: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4",
68+
storageKeys: [
69+
"0x0000000000000000000000000000000000000000000000000000000000000001",
70+
],
71+
},
72+
],
73+
},
74+
});
75+
76+
expect(res.result).toEqual(
77+
"0x01f8a481892a80830157c094152e208d08cd3ea1aa5d179b2e3eba7d1a733ef488016345785d8a000080f838f794152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4e1a0000000000000000000000000000000000000000000000000000000000000000101a0f690bdfb3b8431125dcb77933d3ebb0e5ae05bbd04ad83fa47b2f524013d4c0aa0096ca32df9a7586a4a11ebb72ce8e1902d633976a56ca184ae5009ae53c6bd16",
78+
);
79+
});
80+
81+
test("Sign an eip-4844 transaction", async () => {
82+
const { engine, backendWallet } = await setup();
83+
84+
const res = await engine.backendWallet.signTransaction(backendWallet, {
85+
transaction: {
86+
type: 3,
87+
chainId: 137,
88+
to: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4",
89+
nonce: "42",
90+
gasLimit: "88000",
91+
maxFeePerGas: "2000000000",
92+
maxPriorityFeePerGas: "200000000",
93+
value: "100000000000000000",
94+
accessList: [
95+
{
96+
address: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4",
97+
storageKeys: [
98+
"0x0000000000000000000000000000000000000000000000000000000000000001",
99+
],
100+
},
101+
],
102+
},
103+
});
104+
105+
expect(res.result).toEqual(
106+
"0x03f8af81892a840bebc2008477359400830157c094152e208d08cd3ea1aa5d179b2e3eba7d1a733ef488016345785d8a000080f838f794152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4e1a0000000000000000000000000000000000000000000000000000000000000000180c001a0ec4fd401847092409ffb584cc34e816a322d0bc20f3599b4fe0a0182947fe5bea048daaf620c8b765c07b16ba083c457a90fa54062039d5d1c484e81d1577cc642",
107+
);
108+
});
109+
110+
test("Sign an eip-7702 transaction", async () => {
111+
const { engine, backendWallet } = await setup();
112+
113+
const res = await engine.backendWallet.signTransaction(backendWallet, {
114+
transaction: {
115+
type: 4,
116+
chainId: 137,
117+
to: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4",
118+
nonce: "42",
119+
gasLimit: "88000",
120+
maxFeePerGas: "2000000000",
121+
maxPriorityFeePerGas: "200000000",
122+
value: "100000000000000000",
123+
accessList: [
124+
{
125+
address: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4",
126+
storageKeys: [
127+
"0x0000000000000000000000000000000000000000000000000000000000000001",
128+
],
129+
},
130+
],
131+
customData: {
132+
someCustomField: "customValue",
133+
},
134+
},
135+
});
136+
137+
expect(res.result).toEqual(
138+
"0x04f8ae81892a840bebc2008477359400830157c094152e208d08cd3ea1aa5d179b2e3eba7d1a733ef488016345785d8a000080f838f794152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4e1a00000000000000000000000000000000000000000000000000000000000000001c080a028b866bcf94201d63d71d46505a17a9341d2c7c0f25f98f8e99d5a045b6dd342a03e8807e857830b3e09b300a87c7fedacecc81c3e2222a017be2e0573be011977",
139+
);
140+
});
141+
});

0 commit comments

Comments
 (0)