Skip to content

Commit

Permalink
chore: Update signTransaction to v5 SDK (#718)
Browse files Browse the repository at this point in the history
* fix: Return 400 on invalid wallet

* fix: Update signTransaction route to use v5 SDK

* fix test

* remove try catch
  • Loading branch information
arcoraven authored Oct 24, 2024
1 parent 873c0ef commit c73c18d
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 23 deletions.
49 changes: 39 additions & 10 deletions src/server/routes/backend-wallet/signTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { Static, Type } from "@sinclair/typebox";
import { FastifyInstance } from "fastify";
import { Type, type Static } from "@sinclair/typebox";
import type { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import { getWallet } from "../../../utils/cache/getWallet";
import type { Hex } from "thirdweb";
import { getAccount } from "../../../utils/account";
import {
getChecksumAddress,
maybeBigInt,
maybeInt,
} from "../../../utils/primitiveTypes";
import { toTransactionType } from "../../../utils/sdk";
import { createCustomError } from "../../middleware/error";
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";
import { walletHeaderSchema } from "../../schemas/wallet";

const requestBodySchema = Type.Object({
transaction: Type.Object({
to: Type.Optional(Type.String()),
from: Type.Optional(Type.String()),
nonce: Type.Optional(Type.String()),
gasLimit: Type.Optional(Type.String()),
gasPrice: Type.Optional(Type.String()),
Expand All @@ -19,7 +26,6 @@ const requestBodySchema = Type.Object({
accessList: Type.Optional(Type.Any()),
maxFeePerGas: Type.Optional(Type.String()),
maxPriorityFeePerGas: Type.Optional(Type.String()),
customData: Type.Optional(Type.Record(Type.String(), Type.Any())),
ccipReadEnabled: Type.Optional(Type.Boolean()),
}),
});
Expand Down Expand Up @@ -52,16 +58,39 @@ export async function signTransaction(fastify: FastifyInstance) {
const { "x-backend-wallet-address": walletAddress } =
request.headers as Static<typeof walletHeaderSchema>;

const wallet = await getWallet({
const account = await getAccount({
chainId: 1,
walletAddress,
from: getChecksumAddress(walletAddress),
});
if (!account.signTransaction) {
throw createCustomError(
'This backend wallet does not support "signTransaction".',
StatusCodes.BAD_REQUEST,
"SIGN_TRANSACTION_UNIMPLEMENTED",
);
}

const signer = await wallet.getSigner();
const signedMessage = await signer.signTransaction(transaction);
// @TODO: Assert type to viem TransactionSerializable.
const serializableTransaction: any = {
chainId: transaction.chainId,
to: getChecksumAddress(transaction.to),
nonce: maybeInt(transaction.nonce),
gas: maybeBigInt(transaction.gasLimit),
gasPrice: maybeBigInt(transaction.gasPrice),
data: transaction.data as Hex | undefined,
value: maybeBigInt(transaction.value),
type: transaction.type
? toTransactionType(transaction.type)
: undefined,
accessList: transaction.accessList,
maxFeePerGas: maybeBigInt(transaction.maxFeePerGas),
maxPriorityFeePerGas: maybeBigInt(transaction.maxPriorityFeePerGas),
ccipReadEnabled: transaction.ccipReadEnabled,
};
const signature = await account.signTransaction(serializableTransaction);

reply.status(StatusCodes.OK).send({
result: signedMessage,
result: signature,
});
},
});
Expand Down
8 changes: 4 additions & 4 deletions src/server/routes/transaction/blockchain/getReceipt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { stringify } from "thirdweb/utils";
import type { TransactionReceipt } from "viem";
import { getChain } from "../../../../utils/chain";
import {
fromTransactionStatus,
fromTransactionType,
thirdwebClient,
toTransactionStatus,
toTransactionType,
} from "../../../../utils/sdk";
import { createCustomError } from "../../../middleware/error";
import { AddressSchema, TransactionHashSchema } from "../../../schemas/address";
Expand Down Expand Up @@ -166,8 +166,8 @@ export async function getTransactionReceipt(fastify: FastifyInstance) {
cumulativeGasUsed: toHex(receipt.cumulativeGasUsed),
effectiveGasPrice: toHex(receipt.effectiveGasPrice),
blockNumber: Number(receipt.blockNumber),
type: toTransactionType(receipt.type),
status: toTransactionStatus(receipt.status),
type: fromTransactionType(receipt.type),
status: fromTransactionStatus(receipt.status),
},
});
},
Expand Down
7 changes: 3 additions & 4 deletions src/server/schemas/sharedApiSchemas.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Static, Type } from "@sinclair/typebox";
import { Type, type Static } from "@sinclair/typebox";
import { PREBUILT_CONTRACTS_MAP } from "@thirdweb-dev/sdk";
import { RouteGenericInterface } from "fastify";
import { FastifySchema } from "fastify/types/schema";
import type { RouteGenericInterface } from "fastify";
import type { FastifySchema } from "fastify/types/schema";
import { StatusCodes } from "http-status-codes";
import { AddressSchema } from "./address";

Expand Down Expand Up @@ -182,7 +182,6 @@ export const erc20ContractParamSchema = Type.Object({
}),
contractAddress: {
...AddressSchema,
examples: ["0x365b83D67D5539C6583b9c0266A548926Bf216F4"],
description: "ERC20 contract address",
},
});
Expand Down
2 changes: 1 addition & 1 deletion src/server/schemas/wallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const walletWithAAHeaderSchema = Type.Object({
"x-account-factory-address": Type.Optional({
...AddressSchema,
description:
"Smart account factory address. If omitted, engine will try to resolve it from the chain.",
"Smart account factory address. If omitted, Engine will try to resolve it from the contract.",
}),
"x-account-salt": Type.Optional(
Type.String({
Expand Down
2 changes: 2 additions & 0 deletions src/utils/primitiveTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { Address } from "thirdweb";
import { checksumAddress } from "thirdweb/utils";

export const maybeBigInt = (val?: string) => (val ? BigInt(val) : undefined);
export const maybeInt = (val?: string) =>
val ? Number.parseInt(val) : undefined;

// These overloads hint TS at the response type (ex: Address if `val` is Address).
export function normalizeAddress(val: Address): Address;
Expand Down
15 changes: 12 additions & 3 deletions src/utils/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { sha256HexSync } from "@thirdweb-dev/crypto";
import { createThirdwebClient } from "thirdweb";
import { TransactionReceipt } from "thirdweb/dist/types/transaction/types";
import type { TransactionType } from "viem";
import { env } from "./env";

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

export const toTransactionStatus = (status: "success" | "reverted"): number =>
export const fromTransactionStatus = (status: "success" | "reverted") =>
status === "success" ? 1 : 0;

export const toTransactionType = (type: TransactionReceipt["type"]): number => {
export const fromTransactionType = (type: TransactionType) => {
if (type === "legacy") return 0;
if (type === "eip1559") return 1;
if (type === "eip2930") return 2;
if (type === "eip4844") return 3;
if (type === "eip7702") return 4;
throw new Error(`Unexpected transaction type ${type}`);
};

export const toTransactionType = (value: number) => {
if (value === 0) return "legacy";
if (value === 1) return "eip1559";
if (value === 2) return "eip2930";
if (value === 3) return "eip4844";
if (value === 4) return "eip7702";
throw new Error(`Unexpected transaction type number ${value}`);
};
3 changes: 2 additions & 1 deletion test/e2e/.env.test.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
THIRDWEB_API_SECRET_KEY=""
ENGINE_ACCESS_TOKEN=""
ENGINE_URL=""
ENGINE_URL="http://127.0.0.1:3005"
ANVIL_URL=""
Empty file removed test/e2e/chain.test.ts
Empty file.
141 changes: 141 additions & 0 deletions test/e2e/tests/sign-transaction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { describe, expect, test } from "bun:test";
import { setup } from "./setup";

describe("signTransaction route", () => {
test("Sign a legacy transaction", async () => {
const { engine, backendWallet } = await setup();

const res = await engine.backendWallet.signTransaction(backendWallet, {
transaction: {
type: 0,
chainId: 1,
to: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4",
nonce: "42",
gasLimit: "88000",
gasPrice: "2000000000",
value: "100000000000000000",
},
});

expect(res.result).toEqual(
"0xf86c2a8477359400830157c094152e208d08cd3ea1aa5d179b2e3eba7d1a733ef488016345785d8a00008026a05da3d31d9cfbb4026b6e187c81952199d567e182d9c2ecc72acf98e4e6ce4875a03b2815b79881092ab5a4f74e6725081d652becad8495b815c14abb56cc782041",
);
});

test("Sign an eip-1559 transaction", async () => {
const { engine, backendWallet } = await setup();

const res = await engine.backendWallet.signTransaction(backendWallet, {
transaction: {
type: 1,
chainId: 137,
to: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4",
nonce: "42",
gasLimit: "88000",
maxFeePerGas: "2000000000",
maxPriorityFeePerGas: "200000000",
value: "100000000000000000",
accessList: [
{
address: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4",
storageKeys: [
"0x0000000000000000000000000000000000000000000000000000000000000001",
],
},
],
},
});

expect(res.result).toEqual(
"0x02f8ad81892a840bebc2008477359400830157c094152e208d08cd3ea1aa5d179b2e3eba7d1a733ef488016345785d8a000080f838f794152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4e1a0000000000000000000000000000000000000000000000000000000000000000180a050fd32589ec2e2b49b3bce79d9420474115490ea0693ada75a62d003c6ada1aaa06fbc93d08a7604fbca5c31af92a662ff6be3b5a9f75214b7cd5db5feab2fc444",
);
});

test("Sign an eip-2930 transaction", async () => {
const { engine, backendWallet } = await setup();

const res = await engine.backendWallet.signTransaction(backendWallet, {
transaction: {
type: 2,
chainId: 137,
to: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4",
nonce: "42",
gasLimit: "88000",
value: "100000000000000000",
accessList: [
{
address: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4",
storageKeys: [
"0x0000000000000000000000000000000000000000000000000000000000000001",
],
},
],
},
});

expect(res.result).toEqual(
"0x01f8a481892a80830157c094152e208d08cd3ea1aa5d179b2e3eba7d1a733ef488016345785d8a000080f838f794152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4e1a0000000000000000000000000000000000000000000000000000000000000000101a0f690bdfb3b8431125dcb77933d3ebb0e5ae05bbd04ad83fa47b2f524013d4c0aa0096ca32df9a7586a4a11ebb72ce8e1902d633976a56ca184ae5009ae53c6bd16",
);
});

test("Sign an eip-4844 transaction", async () => {
const { engine, backendWallet } = await setup();

const res = await engine.backendWallet.signTransaction(backendWallet, {
transaction: {
type: 3,
chainId: 137,
to: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4",
nonce: "42",
gasLimit: "88000",
maxFeePerGas: "2000000000",
maxPriorityFeePerGas: "200000000",
value: "100000000000000000",
accessList: [
{
address: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4",
storageKeys: [
"0x0000000000000000000000000000000000000000000000000000000000000001",
],
},
],
},
});

expect(res.result).toEqual(
"0x03f8af81892a840bebc2008477359400830157c094152e208d08cd3ea1aa5d179b2e3eba7d1a733ef488016345785d8a000080f838f794152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4e1a0000000000000000000000000000000000000000000000000000000000000000180c001a0ec4fd401847092409ffb584cc34e816a322d0bc20f3599b4fe0a0182947fe5bea048daaf620c8b765c07b16ba083c457a90fa54062039d5d1c484e81d1577cc642",
);
});

test("Sign an eip-7702 transaction", async () => {
const { engine, backendWallet } = await setup();

const res = await engine.backendWallet.signTransaction(backendWallet, {
transaction: {
type: 4,
chainId: 137,
to: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4",
nonce: "42",
gasLimit: "88000",
maxFeePerGas: "2000000000",
maxPriorityFeePerGas: "200000000",
value: "100000000000000000",
accessList: [
{
address: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4",
storageKeys: [
"0x0000000000000000000000000000000000000000000000000000000000000001",
],
},
],
customData: {
someCustomField: "customValue",
},
},
});

expect(res.result).toEqual(
"0x04f8ae81892a840bebc2008477359400830157c094152e208d08cd3ea1aa5d179b2e3eba7d1a733ef488016345785d8a000080f838f794152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4e1a00000000000000000000000000000000000000000000000000000000000000001c080a028b866bcf94201d63d71d46505a17a9341d2c7c0f25f98f8e99d5a045b6dd342a03e8807e857830b3e09b300a87c7fedacecc81c3e2222a017be2e0573be011977",
);
});
});

0 comments on commit c73c18d

Please sign in to comment.