Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdk/src/services/BackendWalletService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,7 @@ export class BackendWalletService {
requestBody: {
message: string;
isBytes?: boolean;
chainId?: number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note from our convo: can you check if signing on different chains result in the same output. If so, we could default to a testnet (Sepolia?).

},
xIdempotencyKey?: string,
): CancelablePromise<{
Expand Down
14 changes: 9 additions & 5 deletions src/db/wallets/createWalletDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ type CreateWalletDetailsParams = {
awsKmsAccessKeyId: string;
accountSignerAddress: Address;

accountFactoryAddress?: Address;
accountFactoryAddress: Address | undefined;
entrypointAddress: Address | undefined;
}
| {
type: "smart:gcp-kms";
Expand All @@ -48,14 +49,16 @@ type CreateWalletDetailsParams = {
gcpApplicationCredentialEmail: string;
accountSignerAddress: Address;

accountFactoryAddress?: Address;
accountFactoryAddress: Address | undefined;
entrypointAddress: Address | undefined;
}
| {
type: "smart:local";
encryptedJson: string; // ENCRYPTION IS NOT HANDLED HERE, process privatekey with legacyLocalCrytpo before passing to this function
accountSignerAddress: Address;

accountFactoryAddress?: Address;
accountFactoryAddress: Address | undefined;
entrypointAddress: Address | undefined;
}
);

Expand Down Expand Up @@ -125,6 +128,7 @@ export const createWalletDetails = async ({

accountFactoryAddress:
walletDetails.accountFactoryAddress?.toLowerCase(),
entrypointAddress: walletDetails.entrypointAddress?.toLowerCase(),
},
});
}
Expand All @@ -143,6 +147,7 @@ export const createWalletDetails = async ({

accountFactoryAddress:
walletDetails.accountFactoryAddress?.toLowerCase(),
entrypointAddress: walletDetails.entrypointAddress?.toLowerCase(),
},
});
}
Expand All @@ -156,11 +161,10 @@ export const createWalletDetails = async ({

accountFactoryAddress:
walletDetails.accountFactoryAddress?.toLowerCase(),
entrypointAddress: walletDetails.entrypointAddress?.toLowerCase(),
},
});
}

// we will never reach here
// this helps typescript understand that this function will always return
throw new Error("Unsupported wallet type");
};
26 changes: 21 additions & 5 deletions src/db/wallets/getWalletDetails.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getAddress } from "thirdweb";
import { z } from "zod";
import type { PrismaTransaction } from "../../schema/prisma";
import { getConfig } from "../../utils/cache/getConfig";
Expand All @@ -17,14 +18,31 @@ export class WalletDetailsError extends Error {
}
}

/**
* Use the Zod schema to validate the EVM address.
* Uses getAddress from thirdweb/utils to validate the address.
*/
const zodEvmAddressSchema = z.string().transform((address, ctx) => {
try {
return getAddress(address);
} catch {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Invalid EVM address format",
});
return z.NEVER;
}
});

const baseWalletPartialSchema = z.object({
address: z.string(),
address: zodEvmAddressSchema,
label: z.string().nullable(),
});

const smartWalletPartialSchema = z.object({
accountSignerAddress: z.string(),
accountFactoryAddress: z.string().nullable(),
accountSignerAddress: zodEvmAddressSchema,
accountFactoryAddress: zodEvmAddressSchema.nullable(),
entrypointAddress: zodEvmAddressSchema.nullable(),
});

const localWalletSchema = z
Expand All @@ -44,7 +62,6 @@ const smartLocalWalletSchema = localWalletSchema
const awsKmsWalletSchema = z
.object({
type: z.literal("aws-kms"),
address: z.string(),
awsKmsArn: z.string(),
awsKmsSecretAccessKey: z.string(),
awsKmsAccessKeyId: z.string(),
Expand All @@ -61,7 +78,6 @@ const smartAwsKmsWalletSchema = awsKmsWalletSchema
const gcpKmsWalletSchema = z
.object({
type: z.literal("gcp-kms"),
address: z.string(),
gcpKmsResourcePath: z.string(),
gcpApplicationCredentialPrivateKey: z.string(),
gcpApplicationCredentialEmail: z.string(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "wallet_details" ADD COLUMN "entrypointAddress" TEXT;
1 change: 1 addition & 0 deletions src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ model WalletDetails {
// Smart Backend Wallet
accountSignerAddress String? @map("accountSignerAddress") /// this, and either local, aws or gcp encryptedJson, are required for smart wallet
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This naming confused me because account made me think this is the same as smart account address, esp when next to accountFactoryAddress. I think these names are clearer:

  • signerAddress
  • adminAddress

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the WalletDetails schema is very denormalized, so in the interest of maximum clarity, I think having an account prefix is better here.

Between signer and admin, I think signer is better. It leaves us with more flexibility later to allow importing SBW if we want.

accountFactoryAddress String? @map("accountFactoryAddress") /// optional even for smart wallet, if not available default factory will be used
entrypointAddress String? @map("entrypointAddress") /// optional even for smart wallet, if not available SDK will use default entrypoint

@@map("wallet_details")
}
Expand Down
25 changes: 16 additions & 9 deletions src/server/routes/backend-wallet/create.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Type, type Static } from "@sinclair/typebox";
import type { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import { DEFAULT_ACCOUNT_FACTORY_V0_6 } from "thirdweb/wallets/smart";
import { getAddress } from "thirdweb";
import {
DEFAULT_ACCOUNT_FACTORY_V0_7,
ENTRYPOINT_ADDRESS_v0_7,
} from "thirdweb/wallets/smart";
import { WalletType } from "../../../schema/wallet";
import { getConfig } from "../../../utils/cache/getConfig";
import { createCustomError } from "../../middleware/error";
Expand All @@ -15,7 +19,7 @@ import {
CreateGcpKmsWalletError,
createGcpKmsWalletDetails,
} from "../../utils/wallets/createGcpKmsWallet";
import { createAndStoreLocalWallet } from "../../utils/wallets/createLocalWallet";
import { createLocalWalletDetails } from "../../utils/wallets/createLocalWallet";
import {
createSmartAwsWalletDetails,
createSmartGcpWalletDetails,
Expand Down Expand Up @@ -78,7 +82,7 @@ export const createBackendWallet = async (fastify: FastifyInstance) => {

switch (walletType) {
case WalletType.local:
walletAddress = await createAndStoreLocalWallet({ label });
walletAddress = await createLocalWalletDetails({ label });
break;
case WalletType.awsKms:
try {
Expand Down Expand Up @@ -112,10 +116,11 @@ export const createBackendWallet = async (fastify: FastifyInstance) => {
try {
const smartAwsWallet = await createSmartAwsWalletDetails({
label,
accountFactoryAddress: DEFAULT_ACCOUNT_FACTORY_V0_6,
accountFactoryAddress: DEFAULT_ACCOUNT_FACTORY_V0_7,
entrypointAddress: ENTRYPOINT_ADDRESS_v0_7,
});

walletAddress = smartAwsWallet.address;
walletAddress = getAddress(smartAwsWallet.address);
} catch (e) {
if (e instanceof CreateAwsKmsWalletError) {
throw createCustomError(
Expand All @@ -131,9 +136,10 @@ export const createBackendWallet = async (fastify: FastifyInstance) => {
try {
const smartGcpWallet = await createSmartGcpWalletDetails({
label,
accountFactoryAddress: DEFAULT_ACCOUNT_FACTORY_V0_6,
accountFactoryAddress: DEFAULT_ACCOUNT_FACTORY_V0_7,
entrypointAddress: ENTRYPOINT_ADDRESS_v0_7,
});
walletAddress = smartGcpWallet.address;
walletAddress = getAddress(smartGcpWallet.address);
} catch (e) {
if (e instanceof CreateGcpKmsWalletError) {
throw createCustomError(
Expand All @@ -149,9 +155,10 @@ export const createBackendWallet = async (fastify: FastifyInstance) => {
{
const smartLocalWallet = await createSmartLocalWalletDetails({
label,
accountFactoryAddress: DEFAULT_ACCOUNT_FACTORY_V0_6,
accountFactoryAddress: DEFAULT_ACCOUNT_FACTORY_V0_7,
entrypointAddress: ENTRYPOINT_ADDRESS_v0_7,
});
walletAddress = smartLocalWallet.address;
walletAddress = getAddress(smartLocalWallet.address);
}
break;
}
Expand Down
6 changes: 4 additions & 2 deletions src/server/routes/backend-wallet/signMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Type, type Static } from "@sinclair/typebox";
import type { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import { isHex, type Hex } from "thirdweb";
import { arbitrumSepolia } from "thirdweb/chains";
import { getAccount } from "../../../utils/account";
import { getChecksumAddress } from "../../../utils/primitiveTypes";
import { createCustomError } from "../../middleware/error";
Expand All @@ -11,6 +12,7 @@ import { walletHeaderSchema } from "../../schemas/wallet";
const requestBodySchema = Type.Object({
message: Type.String(),
isBytes: Type.Optional(Type.Boolean()),
chainId: Type.Optional(Type.Number()),
});

const responseBodySchema = Type.Object({
Expand All @@ -37,7 +39,7 @@ export async function signMessageRoute(fastify: FastifyInstance) {
},
},
handler: async (request, reply) => {
const { message, isBytes } = request.body;
const { message, isBytes, chainId } = request.body;
const { "x-backend-wallet-address": walletAddress } =
request.headers as Static<typeof walletHeaderSchema>;

Expand All @@ -50,7 +52,7 @@ export async function signMessageRoute(fastify: FastifyInstance) {
}

const account = await getAccount({
chainId: 1,
chainId: chainId ?? arbitrumSepolia.id,
from: getChecksumAddress(walletAddress),
});
const messageToSign = isBytes ? { raw: message as Hex } : message;
Expand Down
16 changes: 9 additions & 7 deletions src/server/utils/wallets/createAwsKmsWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ export const createAwsKmsWalletDetails = async ({
* All optional parameters are overrides for the configuration in the database
* If any required parameter cannot be resolved from either the configuration or the overrides, an error is thrown.
*/
export const createAwsKmsKey = async (params: Partial<AwsKmsWalletParams>) => {
let kmsWalletParams: AwsKmsWalletParams;
export const createAwsKmsKey = async (
partialParams: Partial<AwsKmsWalletParams>,
) => {
let params: AwsKmsWalletParams;
try {
kmsWalletParams = await fetchAwsKmsWalletParams(params);
params = await fetchAwsKmsWalletParams(partialParams);
} catch (e) {
if (e instanceof FetchAwsKmsWalletParamsError) {
throw new CreateAwsKmsWalletError(e.message);
Expand All @@ -51,10 +53,10 @@ export const createAwsKmsKey = async (params: Partial<AwsKmsWalletParams>) => {
}

const client = new KMSClient({
region: kmsWalletParams.awsRegion,
region: params.awsRegion,
credentials: {
accessKeyId: kmsWalletParams.awsAccessKeyId,
secretAccessKey: kmsWalletParams.awsSecretAccessKey,
accessKeyId: params.awsAccessKeyId,
secretAccessKey: params.awsSecretAccessKey,
},
});

Expand All @@ -75,6 +77,6 @@ export const createAwsKmsKey = async (params: Partial<AwsKmsWalletParams>) => {

return {
awsKmsArn,
params: kmsWalletParams,
params: params,
};
};
6 changes: 3 additions & 3 deletions src/server/utils/wallets/createLocalWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface CreateLocalWallet {
* Create a local wallet with a random private key
* Does not store the wallet in the database
*/
export const createLocalWallet = async () => {
export const createRandomLocalWallet = async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "random" sounds unclear to me (especially since the other analogous KMS ones don't have it). createLocalWallet was fine, or generateLocalWallet?

const pk = generatePrivateKey();
const account = privateKeyToAccount({
client: thirdwebClient,
Expand Down Expand Up @@ -44,10 +44,10 @@ export const createLocalWallet = async () => {
/**
* Creates a local wallet and stores it in the database
*/
export const createAndStoreLocalWallet = async ({
export const createLocalWalletDetails = async ({
label,
}: CreateLocalWallet): Promise<string> => {
const { account, encryptedJson } = await createLocalWallet();
const { account, encryptedJson } = await createRandomLocalWallet();

await createWalletDetails({
type: "local",
Expand Down
Loading