Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
30 changes: 22 additions & 8 deletions src/db/wallets/getWalletDetails.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import LRUMap from "mnemonist/lru-map";
import { getAddress } from "thirdweb";
import { z } from "zod";
import type { PrismaTransaction } from "../../schema/prisma";
Expand All @@ -8,7 +9,7 @@ import { getPrismaWithPostgresTx } from "../client";

interface GetWalletDetailsParams {
pgtx?: PrismaTransaction;
address: string;
walletAddress: string;
}

export class WalletDetailsError extends Error {
Expand Down Expand Up @@ -130,6 +131,8 @@ export type SmartBackendWalletType = (typeof SmartBackendWalletTypes)[number];
export type BackendWalletType = (typeof BackendWalletTypes)[number];
export type ParsedWalletDetails = z.infer<typeof walletDetailsSchema>;

export const walletDetailsCache = new LRUMap<string, ParsedWalletDetails>(2048);

/**
* Return the wallet details for the given address.
*
Expand All @@ -143,20 +146,28 @@ export type ParsedWalletDetails = z.infer<typeof walletDetailsSchema>;
*/
export const getWalletDetails = async ({
pgtx,
address,
walletAddress: _walletAddress,
}: GetWalletDetailsParams) => {
// Wallet details are stored in lowercase.
const walletAddress = _walletAddress.toLowerCase();
Copy link
Member

Choose a reason for hiding this comment

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

ugh would love for all of us to align on lower vs checksum


const cachedDetails = walletDetailsCache.get(walletAddress);
if (cachedDetails) {
return cachedDetails;
}

const prisma = getPrismaWithPostgresTx(pgtx);
const config = await getConfig();

const walletDetails = await prisma.walletDetails.findUnique({
where: {
address: address.toLowerCase(),
address: walletAddress,
},
});

if (!walletDetails) {
throw new WalletDetailsError(
`No wallet details found for address ${address}`,
`No wallet details found for address ${walletAddress}`,
);
}

Expand All @@ -167,7 +178,7 @@ export const getWalletDetails = async ({
) {
if (!walletDetails.awsKmsArn) {
throw new WalletDetailsError(
`AWS KMS ARN is missing for the wallet with address ${address}`,
`AWS KMS ARN is missing for the wallet with address ${walletAddress}`,
);
}

Expand All @@ -188,7 +199,7 @@ export const getWalletDetails = async ({
) {
if (!walletDetails.gcpKmsResourcePath) {
throw new WalletDetailsError(
`GCP KMS resource path is missing for the wallet with address ${address}`,
`GCP KMS resource path is missing for the wallet with address ${walletAddress}`,
);
}

Expand All @@ -209,14 +220,17 @@ export const getWalletDetails = async ({

// zod schema can validate all necessary fields are populated after decryption
try {
return walletDetailsSchema.parse(walletDetails, {
const result = walletDetailsSchema.parse(walletDetails, {
errorMap: (issue) => {
const fieldName = issue.path.join(".");
return {
message: `${fieldName} is necessary for wallet ${address} of type ${walletDetails.type}, but not found in wallet details or configuration`,
message: `${fieldName} is necessary for wallet ${walletAddress} of type ${walletDetails.type}, but not found in wallet details or configuration`,
};
},
});

walletDetailsCache.set(walletAddress, result);
return result;
} catch (e) {
if (e instanceof z.ZodError) {
throw new WalletDetailsError(
Expand Down
2 changes: 1 addition & 1 deletion src/server/routes/backend-wallet/signMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export async function signMessageRoute(fastify: FastifyInstance) {
}

const walletDetails = await getWalletDetails({
address: walletAddress,
walletAddress,
});

if (isSmartBackendWallet(walletDetails) && !chainId) {
Expand Down
8 changes: 6 additions & 2 deletions src/server/utils/wallets/getLocalWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ export const getLocalWallet = async ({
});

// If that works, save the wallet using the encryption password for the future
const walletDetails = await getWalletDetails({ address: walletAddress });
const walletDetails = await getWalletDetails({
walletAddress,
});

logger({
service: "worker",
Expand All @@ -73,7 +75,9 @@ export const getLocalWallet = async ({
export const getLocalWalletAccount = async (
walletAddress: Address,
): Promise<Account> => {
const walletDetails = await getWalletDetails({ address: walletAddress });
const walletDetails = await getWalletDetails({
walletAddress,
});

if (walletDetails.type !== "local") {
throw new Error(`Local Wallet not found for address ${walletAddress}`);
Expand Down
4 changes: 2 additions & 2 deletions src/utils/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const getAccount = async (args: {
}

const walletDetails = await getWalletDetails({
address: from,
walletAddress: from,
});

const { account } = await walletDetailsToAccount({ walletDetails, chain });
Expand Down Expand Up @@ -180,7 +180,7 @@ export const getSmartBackendWalletAdminAccount = async ({
}

const walletDetails = await getWalletDetails({
address: accountAddress,
walletAddress: accountAddress,
});

if (!isSmartBackendWallet(walletDetails)) {
Expand Down
4 changes: 4 additions & 0 deletions src/utils/cache/clearCache.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { walletDetailsCache } from "../../db/wallets/getWalletDetails";
import type { env } from "../env";
import { accessTokenCache } from "./accessToken";
import { invalidateConfig } from "./getConfig";
import { sdkCache } from "./getSdk";
import { smartWalletsCache } from "./getSmartWalletV5";
import { walletsCache } from "./getWallet";
import { webhookCache } from "./getWebhook";
import { keypairCache } from "./keypair";
Expand All @@ -15,4 +17,6 @@ export const clearCache = async (
walletsCache.clear();
accessTokenCache.clear();
keypairCache.clear();
smartWalletsCache.clear();
walletDetailsCache.clear();
};
2 changes: 1 addition & 1 deletion src/utils/cache/getWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const getWallet = async <TWallet extends EVMWallet>({
try {
walletDetails = await getWalletDetails({
pgtx,
address: walletAddress,
walletAddress,
});
} catch (e) {
if (e instanceof WalletDetailsError) {
Expand Down
122 changes: 51 additions & 71 deletions src/utils/transaction/insertTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { StatusCodes } from "http-status-codes";
import { randomUUID } from "node:crypto";
import { TransactionDB } from "../../db/transactions/db";
import {
ParsedWalletDetails,
getWalletDetails,
isSmartBackendWallet,
type ParsedWalletDetails,
} from "../../db/wallets/getWalletDetails";
import { doesChainSupportService } from "../../lib/chain/chain-capabilities";
import { createCustomError } from "../../server/middleware/error";
Expand Down Expand Up @@ -43,13 +43,40 @@ export const insertTransaction = async (
}
}

// Get wallet details. For EOA and SBW (v5 endpoints), `from` should return a valid backend wallet.
// For SBW (v4 endpoints), `accountAddress` should return a valid backend wallet.
// Else the provided details are incorrect (user error).
let walletDetails: ParsedWalletDetails | undefined;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the big change @d4mr. To track walletType we need to query walletDetails for all transactions (not just SBW).

let isSmartBackendWalletV4 = false;
try {
walletDetails = await getWalletDetails({
walletAddress: insertedTransaction.from,
});
} catch {}
if (!walletDetails && insertedTransaction.accountAddress) {
try {
walletDetails = await getWalletDetails({
walletAddress: insertedTransaction.accountAddress,
});
isSmartBackendWalletV4 = true;
} catch {}
}
if (!walletDetails) {
throw createCustomError(
"Account not found",
StatusCodes.BAD_REQUEST,
"ACCOUNT_NOT_FOUND",
);
}

let queuedTransaction: QueuedTransaction = {
...insertedTransaction,
status: "queued",
queueId,
queuedAt: new Date(),
resendCount: 0,

walletType: walletDetails.type,
from: getChecksumAddress(insertedTransaction.from),
to: getChecksumAddress(insertedTransaction.to),
signerAddress: getChecksumAddress(insertedTransaction.signerAddress),
Expand All @@ -60,37 +87,34 @@ export const insertTransaction = async (
value: insertedTransaction.value ?? 0n,
};

let walletDetails: ParsedWalletDetails | undefined;
// Handle smart backend wallets details.
if (isSmartBackendWallet(walletDetails)) {
if (
!(await doesChainSupportService(
queuedTransaction.chainId,
"account-abstraction",
))
) {
throw createCustomError(
`Smart backend wallets do not support chain ${queuedTransaction.chainId}.`,
StatusCodes.BAD_REQUEST,
"INVALID_SMART_BACKEND_WALLET_TRANSACTION",
);
}

try {
walletDetails = await getWalletDetails({
address: queuedTransaction.from,
});
queuedTransaction = {
...queuedTransaction,
accountFactoryAddress: walletDetails.accountFactoryAddress ?? undefined,
entrypointAddress: walletDetails.entrypointAddress ?? undefined,
};

// when using the v5 SDK with smart backend wallets, the following values are not set correctly:
// isUserOp is set to false
// account address is blank or the user provided value (this should be the SBW account address)
// from is set to the SBW account address (this should be the SBW signer address)
// these values need to be corrected so the worker can process the transaction
if (isSmartBackendWallet(walletDetails)) {
if (!isSmartBackendWalletV4) {
if (queuedTransaction.accountAddress) {
// Disallow smart backend wallets from sending userOps.
throw createCustomError(
"Smart backend wallets do not support interacting with other smart accounts",
"Smart backend wallets do not support sending transactions with other smart accounts",
StatusCodes.BAD_REQUEST,
"INVALID_SMART_BACKEND_WALLET_INTERACTION",
);
}

if (
!(await doesChainSupportService(
queuedTransaction.chainId,
"account-abstraction",
))
) {
throw createCustomError(
"Chain does not support smart backend wallets",
StatusCodes.BAD_REQUEST,
"SBW_CHAIN_NOT_SUPPORTED",
"INVALID_SMART_BACKEND_WALLET_TRANSACTION",
);
}

Expand All @@ -101,52 +125,8 @@ export const insertTransaction = async (
from: walletDetails.accountSignerAddress,
accountAddress: queuedTransaction.from,
target: queuedTransaction.to,
accountFactoryAddress: walletDetails.accountFactoryAddress ?? undefined,
entrypointAddress: walletDetails.entrypointAddress ?? undefined,
};
}
} catch {
// if wallet details are not found, this is a smart backend wallet using a v4 endpoint
}

if (!walletDetails && queuedTransaction.accountAddress) {
try {
walletDetails = await getWalletDetails({
address: queuedTransaction.accountAddress,
});

// when using v4 SDK with smart backend wallets, the following values are not set correctly:
// entrypointAddress is not set
// accountFactoryAddress is not set
if (walletDetails && isSmartBackendWallet(walletDetails)) {
if (
!(await doesChainSupportService(
queuedTransaction.chainId,
"account-abstraction",
))
) {
throw createCustomError(
"Chain does not support smart backend wallets",
StatusCodes.BAD_REQUEST,
"SBW_CHAIN_NOT_SUPPORTED",
);
}

queuedTransaction = {
...queuedTransaction,
entrypointAddress: walletDetails.entrypointAddress ?? undefined,
accountFactoryAddress:
walletDetails.accountFactoryAddress ?? undefined,
};
}
} catch {
// if wallet details are not found for this either, this backend wallet does not exist at all
throw createCustomError(
"Account not found",
StatusCodes.BAD_REQUEST,
"ACCOUNT_NOT_FOUND",
);
}
}

// Simulate the transaction.
Expand Down
2 changes: 2 additions & 0 deletions src/utils/transaction/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Address, Hex, toSerializableTransaction } from "thirdweb";
import type { TransactionType } from "viem";
import { BackendWalletType } from "../../db/wallets/getWalletDetails";

// TODO: Replace with thirdweb SDK exported type when available.
export type PopulatedTransaction = Awaited<
Expand Down Expand Up @@ -52,6 +53,7 @@ export type InsertedTransaction = {
export type QueuedTransaction = InsertedTransaction & {
status: "queued";

walletType: BackendWalletType;
resendCount: number;
queueId: string;
queuedAt: Date;
Expand Down
5 changes: 4 additions & 1 deletion src/utils/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Static } from "@sinclair/typebox";
import { UsageEvent } from "@thirdweb-dev/service-utils/cf-worker";
import { FastifyInstance } from "fastify";
import { Address, Hex } from "thirdweb";
import { BackendWalletType } from "../db/wallets/getWalletDetails";
import { ADMIN_QUEUES_BASEPATH } from "../server/middleware/adminRoutes";
import { OPENAPI_ROUTES } from "../server/middleware/open-api";
import { contractParamSchema } from "../server/schemas/sharedApiSchemas";
Expand All @@ -22,6 +23,7 @@ export interface ReportUsageParams {
| "api_request";
input: {
chainId?: number;
walletType?: BackendWalletType;
from?: Address;
to?: Address;
value?: bigint;
Expand Down Expand Up @@ -113,6 +115,7 @@ export const reportUsage = (usageEvents: ReportUsageParams[]) => {
action,
clientId: thirdwebClientId,
chainId: input.chainId,
walletType: input.walletType,
walletAddress: input.from,
contractAddress: input.to,
transactionValue: input.value?.toString(),
Expand All @@ -136,7 +139,7 @@ export const reportUsage = (usageEvents: ReportUsageParams[]) => {
logger({
service: "worker",
level: "error",
message: `Error:`,
message: "Error reporting usage event:",
error: e,
});
}
Expand Down