Skip to content
Merged
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
112 changes: 101 additions & 11 deletions src/db/wallets/createWalletDetails.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Address } from "thirdweb";
import type { PrismaTransaction } from "../../schema/prisma";
import { encrypt } from "../../utils/crypto";
import { getPrismaWithPostgresTx } from "../client";
Expand All @@ -8,13 +9,17 @@ type CreateWalletDetailsParams = {
address: string;
label?: string;
} & (
| {
type: "local";
encryptedJson: string; // ENCRYPTION IS NOT HANDLED HERE, process privatekey with legacyLocalCrytpo before passing to this function
}
| {
type: "aws-kms";
awsKmsKeyId?: string; // depcrecated and unused, todo: remove with next breaking change
awsKmsArn: string;

awsKmsSecretAccessKey?: string; // will be encrypted and stored, pass plaintext to this function
awsKmsAccessKeyId?: string;
awsKmsSecretAccessKey: string; // will be encrypted and stored, pass plaintext to this function
awsKmsAccessKeyId: string;
Comment on lines +21 to +22
Copy link
Contributor

Choose a reason for hiding this comment

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

Won't making these fields non-optional be a breaking change? We could ofc update the dashboard, but it'll impact anyone in the slim chance they're automating KMS backend wallet creation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We decided to always denormalize and store credentials in the DB row, regardless of it being an override or coming from config. Users don't have to pass it in, the route fills in this from the config.

}
| {
type: "gcp-kms";
Expand All @@ -24,11 +29,39 @@ type CreateWalletDetailsParams = {
gcpKmsKeyVersionId?: string; // depcrecated and unused, todo: remove with next breaking change
gcpKmsLocationId?: string; // depcrecated and unused, todo: remove with next breaking change

gcpApplicationCredentialPrivateKey?: string; // encrypted
gcpApplicationCredentialEmail?: string;
gcpApplicationCredentialPrivateKey: string; // will be encrypted and stored, pass plaintext to this function
gcpApplicationCredentialEmail: string;
}
| {
type: "smart:aws-kms";
awsKmsArn: string;
awsKmsSecretAccessKey: string; // will be encrypted and stored, pass plaintext to this function
awsKmsAccessKeyId: string;
accountSignerAddress: Address;

accountFactoryAddress?: Address;
}
| {
type: "smart:gcp-kms";
gcpKmsResourcePath: string;
gcpApplicationCredentialPrivateKey: string; // will be encrypted and stored, pass plaintext to this function
gcpApplicationCredentialEmail: string;
accountSignerAddress: Address;

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

accountFactoryAddress?: Address;
}
);

/**
* Create a new WalletDetails row in DB
*/
export const createWalletDetails = async ({
pgtx,
...walletDetails
Expand All @@ -47,15 +80,23 @@ export const createWalletDetails = async ({
);
}

if (walletDetails.type === "local") {
return prisma.walletDetails.create({
data: {
...walletDetails,
address: walletDetails.address.toLowerCase(),
encryptedJson: walletDetails.encryptedJson,
},
});
}

if (walletDetails.type === "aws-kms") {
return prisma.walletDetails.create({
data: {
...walletDetails,
address: walletDetails.address.toLowerCase(),

awsKmsSecretAccessKey: walletDetails.awsKmsSecretAccessKey
? encrypt(walletDetails.awsKmsSecretAccessKey)
: undefined,
awsKmsSecretAccessKey: encrypt(walletDetails.awsKmsSecretAccessKey),
},
});
}
Expand All @@ -66,11 +107,60 @@ export const createWalletDetails = async ({
...walletDetails,
address: walletDetails.address.toLowerCase(),

gcpApplicationCredentialPrivateKey:
walletDetails.gcpApplicationCredentialPrivateKey
? encrypt(walletDetails.gcpApplicationCredentialPrivateKey)
: undefined,
gcpApplicationCredentialPrivateKey: encrypt(
walletDetails.gcpApplicationCredentialPrivateKey,
),
},
});
}

if (walletDetails.type === "smart:aws-kms") {
return prisma.walletDetails.create({
data: {
...walletDetails,

address: walletDetails.address.toLowerCase(),
awsKmsSecretAccessKey: encrypt(walletDetails.awsKmsSecretAccessKey),
accountSignerAddress: walletDetails.accountSignerAddress.toLowerCase(),

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

if (walletDetails.type === "smart:gcp-kms") {
return prisma.walletDetails.create({
data: {
...walletDetails,

address: walletDetails.address.toLowerCase(),
accountSignerAddress: walletDetails.accountSignerAddress.toLowerCase(),

gcpApplicationCredentialPrivateKey: encrypt(
walletDetails.gcpApplicationCredentialPrivateKey,
),

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

if (walletDetails.type === "smart:local") {
return prisma.walletDetails.create({
data: {
...walletDetails,
address: walletDetails.address.toLowerCase(),
accountSignerAddress: walletDetails.accountSignerAddress.toLowerCase(),

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

// we will never reach here
// this helps typescript understand that this function will always return
Copy link
Contributor

Choose a reason for hiding this comment

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

No need for these comments IMO. Should be clear. And if it's not, just make the thrown error message more clear.

throw new Error("Unsupported wallet type");
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "wallet_details" ADD COLUMN "accountFactoryAddress" TEXT,
ADD COLUMN "accountSignerAddress" TEXT;
3 changes: 3 additions & 0 deletions src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ model WalletDetails {
gcpKmsResourcePath String? @map("gcpKmsResourcePath") @db.Text
gcpApplicationCredentialEmail String? @map("gcpApplicationCredentialEmail") /// if not available, default to: Configuration.gcpApplicationCredentialEmail
gcpApplicationCredentialPrivateKey String? @map("gcpApplicationCredentialPrivateKey") /// if not available, default to: Configuration.gcpApplicationCredentialPrivateKey
// 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

@@map("wallet_details")
}
Expand Down
5 changes: 5 additions & 0 deletions src/schema/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,9 @@ export enum WalletType {
local = "local",
awsKms = "aws-kms",
gcpKms = "gcp-kms",

// Smart wallets
smartAwsKms = "smart:aws-kms",
smartGcpKms = "smart:gcp-kms",
smartLocal = "smart:local",
}
64 changes: 57 additions & 7 deletions src/server/routes/backend-wallet/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,25 @@ import { AddressSchema } from "../../schemas/address";
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";
import {
CreateAwsKmsWalletError,
createAwsKmsWallet,
createAndStoreAwsKmsWallet,
} from "../../utils/wallets/createAwsKmsWallet";
import {
CreateGcpKmsWalletError,
createGcpKmsWallet,
createAndStoreGcpKmsWallet,
} from "../../utils/wallets/createGcpKmsWallet";
import { createLocalWallet } from "../../utils/wallets/createLocalWallet";
import { createAndStoreLocalWallet } from "../../utils/wallets/createLocalWallet";
import {
createAndStoreSmartAwsWallet,
createAndStoreSmartGcpWallet,
createAndStoreSmartLocalWallet,
} from "../../utils/wallets/createSmartWallet";

const requestBodySchema = Type.Object({
label: Type.Optional(Type.String()),
type: Type.Optional(
Type.Enum(WalletType, {
description:
"Optional wallet type. If not provided, the default wallet type will be used.",
"Type of new wallet to create. It is recommended to always provide this value. If not provided, the default wallet type will be used.",
}),
),
});
Expand All @@ -30,13 +35,15 @@ const responseSchema = Type.Object({
result: Type.Object({
walletAddress: AddressSchema,
status: Type.String(),
type: Type.Enum(WalletType),
}),
});

responseSchema.example = {
result: {
walletAddress: "0x....",
status: "success",
type: WalletType.local,
},
};

Expand Down Expand Up @@ -70,11 +77,11 @@ export const createBackendWallet = async (fastify: FastifyInstance) => {

switch (walletType) {
case WalletType.local:
walletAddress = await createLocalWallet({ label });
walletAddress = await createAndStoreLocalWallet({ label });
break;
case WalletType.awsKms:
try {
walletAddress = await createAwsKmsWallet({ label });
walletAddress = await createAndStoreAwsKmsWallet({ label });
} catch (e) {
if (e instanceof CreateAwsKmsWalletError) {
throw createCustomError(
Expand All @@ -88,7 +95,7 @@ export const createBackendWallet = async (fastify: FastifyInstance) => {
break;
case WalletType.gcpKms:
try {
walletAddress = await createGcpKmsWallet({ label });
walletAddress = await createAndStoreGcpKmsWallet({ label });
} catch (e) {
if (e instanceof CreateGcpKmsWalletError) {
throw createCustomError(
Expand All @@ -100,11 +107,54 @@ export const createBackendWallet = async (fastify: FastifyInstance) => {
throw e;
}
break;
case WalletType.smartAwsKms:
try {
const smartAwsWallet = await createAndStoreSmartAwsWallet({
label,
});

walletAddress = smartAwsWallet.address;
} catch (e) {
if (e instanceof CreateAwsKmsWalletError) {
throw createCustomError(
e.message,
StatusCodes.BAD_REQUEST,
"CREATE_AWS_KMS_WALLET_ERROR",
);
}
throw e;
}
break;
case WalletType.smartGcpKms:
try {
const smartGcpWallet = await createAndStoreSmartGcpWallet({
label,
});
walletAddress = smartGcpWallet.address;
} catch (e) {
if (e instanceof CreateGcpKmsWalletError) {
throw createCustomError(
e.message,
StatusCodes.BAD_REQUEST,
"CREATE_GCP_KMS_WALLET_ERROR",
);
}
throw e;
}
break;
case WalletType.smartLocal:
walletAddress = (
await createAndStoreSmartLocalWallet({
label,
})
).address;
break;
}

reply.status(StatusCodes.OK).send({
result: {
walletAddress,
type: walletType,
status: "success",
},
});
Expand Down
14 changes: 11 additions & 3 deletions src/server/utils/storage/localStorage.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { AsyncStorage } from "@thirdweb-dev/wallets";
import fs from "fs";
import type { AsyncStorage } from "@thirdweb-dev/wallets";
import fs from "node:fs";
import { prisma } from "../../../db/client";
import { WalletType } from "../../../schema/wallet";
import { logger } from "../../../utils/logger";

/**
* @deprecated
* Deprecated local file storage implementation for use with v4 sdk
* Use `legacyLocalCrypto` for encryption and decryption instead
*/
export class LocalFileStorage implements AsyncStorage {
label?: string;

constructor(private readonly walletAddress: string, label?: string) {
constructor(
private readonly walletAddress: string,
label?: string,
) {
this.walletAddress = walletAddress.toLowerCase();
this.label = label;
}
Expand Down
39 changes: 28 additions & 11 deletions src/server/utils/wallets/createAwsKmsWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from "./fetchAwsKmsWalletParams";
import { importAwsKmsWallet } from "./importAwsKmsWallet";

type CreateAwsKmsWalletParams = {
export type CreateAwsKmsWalletParams = {
label?: string;
} & Partial<AwsKmsWalletParams>;

Expand All @@ -16,15 +16,35 @@ export class CreateAwsKmsWalletError extends Error {}
* Create an AWS KMS wallet, and store it into the database
* 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.
* If credentials (awsAccessKeyId and awsSecretAccessKey) are explicitly provided, they will be stored separately from the global configuration
* Credentials (awsAccessKeyId and awsSecretAccessKey) are explicitly stored separately from the global configuration
*/
export const createAwsKmsWallet = async ({
export const createAndStoreAwsKmsWallet = async ({
label,
...overrides
}: CreateAwsKmsWalletParams): Promise<string> => {
const { awsKmsArn, params } = await createAwsKmsWallet(overrides);

return importAwsKmsWallet({
Copy link
Contributor

Choose a reason for hiding this comment

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

I think our verbs are getting confusing. Let's use fewer unique verbs but add details (even if longer var/fn names) for clarity.

  • create
  • import
  • store

👇

  • createAwsKmsKey
  • createAwsKmsWallet or createAwsKmsWalletDetails or createAwsKmsWalletToDb
  • This function can remain createAwsKmsWallet since that makes sense

awsKmsArn,
label,
crendentials: {
accessKeyId: params.awsAccessKeyId,
secretAccessKey: params.awsSecretAccessKey,
},
});
};

/**
* Creates an AWS KMS wallet and returns the AWS KMS ARN
* 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 createAwsKmsWallet = async (
params: Partial<AwsKmsWalletParams>,
) => {
let kmsWalletParams: AwsKmsWalletParams;
try {
kmsWalletParams = await fetchAwsKmsWalletParams(overrides);
kmsWalletParams = await fetchAwsKmsWalletParams(params);
} catch (e) {
if (e instanceof FetchAwsKmsWalletParamsError) {
throw new CreateAwsKmsWalletError(e.message);
Expand Down Expand Up @@ -54,12 +74,9 @@ export const createAwsKmsWallet = async ({
}

const awsKmsArn = res.KeyMetadata.Arn;
return importAwsKmsWallet({

return {
awsKmsArn,
label,
crendentials: {
accessKeyId: kmsWalletParams.awsAccessKeyId,
secretAccessKey: kmsWalletParams.awsSecretAccessKey,
},
});
params: kmsWalletParams,
};
};
Loading
Loading