Skip to content

Commit 68c21b6

Browse files
committed
smart backend wallet creation + initial flow
1 parent 921449d commit 68c21b6

File tree

18 files changed

+752
-70
lines changed

18 files changed

+752
-70
lines changed

src/db/wallets/createWalletDetails.ts

Lines changed: 101 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Address } from "thirdweb";
12
import type { PrismaTransaction } from "../../schema/prisma";
23
import { encrypt } from "../../utils/crypto";
34
import { getPrismaWithPostgresTx } from "../client";
@@ -8,13 +9,17 @@ type CreateWalletDetailsParams = {
89
address: string;
910
label?: string;
1011
} & (
12+
| {
13+
type: "local";
14+
encryptedJson: string; // ENCRYPTION IS NOT HANDLED HERE, process privatekey with legacyLocalCrytpo before passing to this function
15+
}
1116
| {
1217
type: "aws-kms";
1318
awsKmsKeyId?: string; // depcrecated and unused, todo: remove with next breaking change
1419
awsKmsArn: string;
1520

16-
awsKmsSecretAccessKey?: string; // will be encrypted and stored, pass plaintext to this function
17-
awsKmsAccessKeyId?: string;
21+
awsKmsSecretAccessKey: string; // will be encrypted and stored, pass plaintext to this function
22+
awsKmsAccessKeyId: string;
1823
}
1924
| {
2025
type: "gcp-kms";
@@ -24,11 +29,39 @@ type CreateWalletDetailsParams = {
2429
gcpKmsKeyVersionId?: string; // depcrecated and unused, todo: remove with next breaking change
2530
gcpKmsLocationId?: string; // depcrecated and unused, todo: remove with next breaking change
2631

27-
gcpApplicationCredentialPrivateKey?: string; // encrypted
28-
gcpApplicationCredentialEmail?: string;
32+
gcpApplicationCredentialPrivateKey: string; // will be encrypted and stored, pass plaintext to this function
33+
gcpApplicationCredentialEmail: string;
34+
}
35+
| {
36+
type: "smart:aws-kms";
37+
awsKmsArn: string;
38+
awsKmsSecretAccessKey: string; // will be encrypted and stored, pass plaintext to this function
39+
awsKmsAccessKeyId: string;
40+
accountSignerAddress: Address;
41+
42+
accountFactoryAddress?: Address;
43+
}
44+
| {
45+
type: "smart:gcp-kms";
46+
gcpKmsResourcePath: string;
47+
gcpApplicationCredentialPrivateKey: string; // will be encrypted and stored, pass plaintext to this function
48+
gcpApplicationCredentialEmail: string;
49+
accountSignerAddress: Address;
50+
51+
accountFactoryAddress?: Address;
52+
}
53+
| {
54+
type: "smart:local";
55+
encryptedJson: string; // ENCRYPTION IS NOT HANDLED HERE, process privatekey with legacyLocalCrytpo before passing to this function
56+
accountSignerAddress: Address;
57+
58+
accountFactoryAddress?: Address;
2959
}
3060
);
3161

62+
/**
63+
* Create a new WalletDetails row in DB
64+
*/
3265
export const createWalletDetails = async ({
3366
pgtx,
3467
...walletDetails
@@ -47,15 +80,23 @@ export const createWalletDetails = async ({
4780
);
4881
}
4982

83+
if (walletDetails.type === "local") {
84+
return prisma.walletDetails.create({
85+
data: {
86+
...walletDetails,
87+
address: walletDetails.address.toLowerCase(),
88+
encryptedJson: walletDetails.encryptedJson,
89+
},
90+
});
91+
}
92+
5093
if (walletDetails.type === "aws-kms") {
5194
return prisma.walletDetails.create({
5295
data: {
5396
...walletDetails,
5497
address: walletDetails.address.toLowerCase(),
5598

56-
awsKmsSecretAccessKey: walletDetails.awsKmsSecretAccessKey
57-
? encrypt(walletDetails.awsKmsSecretAccessKey)
58-
: undefined,
99+
awsKmsSecretAccessKey: encrypt(walletDetails.awsKmsSecretAccessKey),
59100
},
60101
});
61102
}
@@ -66,11 +107,60 @@ export const createWalletDetails = async ({
66107
...walletDetails,
67108
address: walletDetails.address.toLowerCase(),
68109

69-
gcpApplicationCredentialPrivateKey:
70-
walletDetails.gcpApplicationCredentialPrivateKey
71-
? encrypt(walletDetails.gcpApplicationCredentialPrivateKey)
72-
: undefined,
110+
gcpApplicationCredentialPrivateKey: encrypt(
111+
walletDetails.gcpApplicationCredentialPrivateKey,
112+
),
113+
},
114+
});
115+
}
116+
117+
if (walletDetails.type === "smart:aws-kms") {
118+
return prisma.walletDetails.create({
119+
data: {
120+
...walletDetails,
121+
122+
address: walletDetails.address.toLowerCase(),
123+
awsKmsSecretAccessKey: encrypt(walletDetails.awsKmsSecretAccessKey),
124+
accountSignerAddress: walletDetails.accountSignerAddress.toLowerCase(),
125+
126+
accountFactoryAddress:
127+
walletDetails.accountFactoryAddress?.toLowerCase(),
128+
},
129+
});
130+
}
131+
132+
if (walletDetails.type === "smart:gcp-kms") {
133+
return prisma.walletDetails.create({
134+
data: {
135+
...walletDetails,
136+
137+
address: walletDetails.address.toLowerCase(),
138+
accountSignerAddress: walletDetails.accountSignerAddress.toLowerCase(),
139+
140+
gcpApplicationCredentialPrivateKey: encrypt(
141+
walletDetails.gcpApplicationCredentialPrivateKey,
142+
),
143+
144+
accountFactoryAddress:
145+
walletDetails.accountFactoryAddress?.toLowerCase(),
73146
},
74147
});
75148
}
149+
150+
if (walletDetails.type === "smart:local") {
151+
return prisma.walletDetails.create({
152+
data: {
153+
...walletDetails,
154+
address: walletDetails.address.toLowerCase(),
155+
accountSignerAddress: walletDetails.accountSignerAddress.toLowerCase(),
156+
157+
accountFactoryAddress:
158+
walletDetails.accountFactoryAddress?.toLowerCase(),
159+
},
160+
});
161+
}
162+
163+
// we will never reach here
164+
// this helps typescript understand that this function will always return
165+
throw new Error("Unsupported wallet type");
76166
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- AlterTable
2+
ALTER TABLE "wallet_details" ADD COLUMN "accountFactoryAddress" TEXT,
3+
ADD COLUMN "accountSignerAddress" TEXT;

src/prisma/schema.prisma

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ model WalletDetails {
9494
gcpKmsResourcePath String? @map("gcpKmsResourcePath") @db.Text
9595
gcpApplicationCredentialEmail String? @map("gcpApplicationCredentialEmail") /// if not available, default to: Configuration.gcpApplicationCredentialEmail
9696
gcpApplicationCredentialPrivateKey String? @map("gcpApplicationCredentialPrivateKey") /// if not available, default to: Configuration.gcpApplicationCredentialPrivateKey
97+
// Smart Backend Wallet
98+
accountSignerAddress String? @map("accountSignerAddress") /// this, and either local, aws or gcp encryptedJson, are required for smart wallet
99+
accountFactoryAddress String? @map("accountFactoryAddress") /// optional even for smart wallet, if not available default factory will be used
97100
98101
@@map("wallet_details")
99102
}

src/schema/wallet.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,9 @@ export enum WalletType {
22
local = "local",
33
awsKms = "aws-kms",
44
gcpKms = "gcp-kms",
5+
6+
// Smart wallets
7+
smartAwsKms = "smart:aws-kms",
8+
smartGcpKms = "smart:gcp-kms",
9+
smartLocal = "smart:local",
510
}

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

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,25 @@ import { AddressSchema } from "../../schemas/address";
88
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";
99
import {
1010
CreateAwsKmsWalletError,
11-
createAwsKmsWallet,
11+
createAndStoreAwsKmsWallet,
1212
} from "../../utils/wallets/createAwsKmsWallet";
1313
import {
1414
CreateGcpKmsWalletError,
15-
createGcpKmsWallet,
15+
createAndStoreGcpKmsWallet,
1616
} from "../../utils/wallets/createGcpKmsWallet";
17-
import { createLocalWallet } from "../../utils/wallets/createLocalWallet";
17+
import { createAndStoreLocalWallet } from "../../utils/wallets/createLocalWallet";
18+
import {
19+
createAndStoreSmartAwsWallet,
20+
createAndStoreSmartGcpWallet,
21+
createAndStoreSmartLocalWallet,
22+
} from "../../utils/wallets/createSmartWallet";
1823

1924
const requestBodySchema = Type.Object({
2025
label: Type.Optional(Type.String()),
2126
type: Type.Optional(
2227
Type.Enum(WalletType, {
2328
description:
24-
"Optional wallet type. If not provided, the default wallet type will be used.",
29+
"Type of new wallet to create. It is recommended to always provide this value. If not provided, the default wallet type will be used.",
2530
}),
2631
),
2732
});
@@ -30,13 +35,15 @@ const responseSchema = Type.Object({
3035
result: Type.Object({
3136
walletAddress: AddressSchema,
3237
status: Type.String(),
38+
type: Type.Enum(WalletType),
3339
}),
3440
});
3541

3642
responseSchema.example = {
3743
result: {
3844
walletAddress: "0x....",
3945
status: "success",
46+
type: WalletType.local,
4047
},
4148
};
4249

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

7178
switch (walletType) {
7279
case WalletType.local:
73-
walletAddress = await createLocalWallet({ label });
80+
walletAddress = await createAndStoreLocalWallet({ label });
7481
break;
7582
case WalletType.awsKms:
7683
try {
77-
walletAddress = await createAwsKmsWallet({ label });
84+
walletAddress = await createAndStoreAwsKmsWallet({ label });
7885
} catch (e) {
7986
if (e instanceof CreateAwsKmsWalletError) {
8087
throw createCustomError(
@@ -88,7 +95,7 @@ export const createBackendWallet = async (fastify: FastifyInstance) => {
8895
break;
8996
case WalletType.gcpKms:
9097
try {
91-
walletAddress = await createGcpKmsWallet({ label });
98+
walletAddress = await createAndStoreGcpKmsWallet({ label });
9299
} catch (e) {
93100
if (e instanceof CreateGcpKmsWalletError) {
94101
throw createCustomError(
@@ -100,11 +107,54 @@ export const createBackendWallet = async (fastify: FastifyInstance) => {
100107
throw e;
101108
}
102109
break;
110+
case WalletType.smartAwsKms:
111+
try {
112+
const smartAwsWallet = await createAndStoreSmartAwsWallet({
113+
label,
114+
});
115+
116+
walletAddress = smartAwsWallet.address;
117+
} catch (e) {
118+
if (e instanceof CreateAwsKmsWalletError) {
119+
throw createCustomError(
120+
e.message,
121+
StatusCodes.BAD_REQUEST,
122+
"CREATE_AWS_KMS_WALLET_ERROR",
123+
);
124+
}
125+
throw e;
126+
}
127+
break;
128+
case WalletType.smartGcpKms:
129+
try {
130+
const smartGcpWallet = await createAndStoreSmartGcpWallet({
131+
label,
132+
});
133+
walletAddress = smartGcpWallet.address;
134+
} catch (e) {
135+
if (e instanceof CreateGcpKmsWalletError) {
136+
throw createCustomError(
137+
e.message,
138+
StatusCodes.BAD_REQUEST,
139+
"CREATE_GCP_KMS_WALLET_ERROR",
140+
);
141+
}
142+
throw e;
143+
}
144+
break;
145+
case WalletType.smartLocal:
146+
walletAddress = (
147+
await createAndStoreSmartLocalWallet({
148+
label,
149+
})
150+
).address;
151+
break;
103152
}
104153

105154
reply.status(StatusCodes.OK).send({
106155
result: {
107156
walletAddress,
157+
type: walletType,
108158
status: "success",
109159
},
110160
});

src/server/utils/storage/localStorage.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1-
import { AsyncStorage } from "@thirdweb-dev/wallets";
2-
import fs from "fs";
1+
import type { AsyncStorage } from "@thirdweb-dev/wallets";
2+
import fs from "node:fs";
33
import { prisma } from "../../../db/client";
44
import { WalletType } from "../../../schema/wallet";
55
import { logger } from "../../../utils/logger";
66

7+
/**
8+
* @deprecated
9+
* Deprecated local file storage implementation for use with v4 sdk
10+
* Use `legacyLocalCrypto` for encryption and decryption instead
11+
*/
712
export class LocalFileStorage implements AsyncStorage {
813
label?: string;
914

10-
constructor(private readonly walletAddress: string, label?: string) {
15+
constructor(
16+
private readonly walletAddress: string,
17+
label?: string,
18+
) {
1119
this.walletAddress = walletAddress.toLowerCase();
1220
this.label = label;
1321
}

src/server/utils/wallets/createAwsKmsWallet.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
} from "./fetchAwsKmsWalletParams";
77
import { importAwsKmsWallet } from "./importAwsKmsWallet";
88

9-
type CreateAwsKmsWalletParams = {
9+
export type CreateAwsKmsWalletParams = {
1010
label?: string;
1111
} & Partial<AwsKmsWalletParams>;
1212

@@ -16,15 +16,35 @@ export class CreateAwsKmsWalletError extends Error {}
1616
* Create an AWS KMS wallet, and store it into the database
1717
* All optional parameters are overrides for the configuration in the database
1818
* If any required parameter cannot be resolved from either the configuration or the overrides, an error is thrown.
19-
* If credentials (awsAccessKeyId and awsSecretAccessKey) are explicitly provided, they will be stored separately from the global configuration
19+
* Credentials (awsAccessKeyId and awsSecretAccessKey) are explicitly stored separately from the global configuration
2020
*/
21-
export const createAwsKmsWallet = async ({
21+
export const createAndStoreAwsKmsWallet = async ({
2222
label,
2323
...overrides
2424
}: CreateAwsKmsWalletParams): Promise<string> => {
25+
const { awsKmsArn, params } = await createAwsKmsWallet(overrides);
26+
27+
return importAwsKmsWallet({
28+
awsKmsArn,
29+
label,
30+
crendentials: {
31+
accessKeyId: params.awsAccessKeyId,
32+
secretAccessKey: params.awsSecretAccessKey,
33+
},
34+
});
35+
};
36+
37+
/**
38+
* Creates an AWS KMS wallet and returns the AWS KMS ARN
39+
* All optional parameters are overrides for the configuration in the database
40+
* If any required parameter cannot be resolved from either the configuration or the overrides, an error is thrown.
41+
*/
42+
export const createAwsKmsWallet = async (
43+
params: Partial<AwsKmsWalletParams>,
44+
) => {
2545
let kmsWalletParams: AwsKmsWalletParams;
2646
try {
27-
kmsWalletParams = await fetchAwsKmsWalletParams(overrides);
47+
kmsWalletParams = await fetchAwsKmsWalletParams(params);
2848
} catch (e) {
2949
if (e instanceof FetchAwsKmsWalletParamsError) {
3050
throw new CreateAwsKmsWalletError(e.message);
@@ -54,12 +74,9 @@ export const createAwsKmsWallet = async ({
5474
}
5575

5676
const awsKmsArn = res.KeyMetadata.Arn;
57-
return importAwsKmsWallet({
77+
78+
return {
5879
awsKmsArn,
59-
label,
60-
crendentials: {
61-
accessKeyId: kmsWalletParams.awsAccessKeyId,
62-
secretAccessKey: kmsWalletParams.awsSecretAccessKey,
63-
},
64-
});
80+
params: kmsWalletParams,
81+
};
6582
};

0 commit comments

Comments
 (0)