Skip to content

Commit b0d862c

Browse files
authored
Add prisma schemas & queries (#118)
* updated roles/permission doc (#115) * Add prisma schemas * swagger UI on Production gives a 404 (#116) * fixed nodemon & retry to not update mined status, let the other process handle it (#117) * Update database queries to use prisma
1 parent 785ce90 commit b0d862c

File tree

11 files changed

+375
-27
lines changed

11 files changed

+375
-27
lines changed

.github/aws_kms_how_to.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ Web3-API supports AWS KMS for signing & sending transactions over any EVM chain.
44

55
1. Create IAM user with programmatic access, see [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html#id_users_create_console) for more details.
66
2. Add create, get, read permission to KMS, see [here](https://docs.aws.amazon.com/kms/latest/developerguide/control-access.html) for more details.
7+
8+
```
9+
Minimum Permissions Required:
10+
---------------------------
11+
kms:CreateKey
12+
kms:GetPublicKey
13+
kms:Sign
14+
kms:CreateAlias
15+
kms:Verify
16+
```
17+
718
3. Create a AWS KMS key, see [here](https://docs.aws.amazon.com/kms/latest/developerguide/create-keys.html) for more details. or, you can use the `/wallet/create` to create a key.
819

920
NOTE:
@@ -27,7 +38,4 @@ Create a `.env` file in the root directory of the project and add the below deta
2738
AWS_ACCESS_KEY_ID=<aws_access_key_id>
2839
AWS_SECRET_ACCESS_KEY=<aws_secret_access_key>
2940
AWS_REGION=<aws_region>
30-
31-
# Required for AWS KMS Admin Wallet
32-
AWS_KMS_KEY_ID=<kms_key_id>
3341
```

.github/google_kms_how_to.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@ Web3-API supports Google KMS for signing & sending transactions over any EVM cha
33
### Steps to set up Google KMS
44

55
1. Enable Google KMS API for your Google project, see [here](https://cloud.google.com/kms/docs/create-encryption-keys#before-you-begin) for more details.
6-
2. Create a Service Account (here)[https://cloud.google.com/iam/docs/service-accounts-create] and create a key under this service account and download the JSON file. This JSON file details will be used to authenticate with Google KMS.
7-
3. Add the below permissions to the service account created in step 2.
6+
2. Create a Service Account (here)[https://cloud.google.com/iam/docs/service-accounts-create]
7+
3. Go to IAM & Admin -> IAM. Select the service account created in step 2 and click `Edit Principal` to add the below roles.
88

99
```
10+
Minimum Roles:
11+
1012
Cloud KMS Admin
1113
Cloud KMS CryptoKey Signer/Verifier
1214
```
1315

14-
4. Create a keyring in Google KMS, see [here](https://cloud.google.com/kms/docs/create-key-ring) for more details.
16+
4. Click on the created Service-Account and go to `Keys` tab.
17+
5. Click `Add Key` -> Create new Key -> select `JSON` & download the JSON file. This JSON file details will be used to authenticate google auth while using Google Cloud KMS.
18+
6. Create a keyring in Google KMS, see [here](https://cloud.google.com/kms/docs/create-key-ring) for more details.
1519

1620
Optional: Create a key in the keyring, see [here](https://cloud.google.com/kms/docs/create-key) for more details. or, you can use the `/wallet/create` to create a key in the keyring.
1721

@@ -24,9 +28,8 @@ Create a `.env` file in the root directory of the project and add the below deta
2428
GOOGLE_APPLICATION_CREDENTIAL_EMAIL=<client_email_from_download_service_account_json>
2529
GOOGLE_APPLICATION_CREDENTIAL_PRIVATE_KEY=<private_key_from_download_service_account_json>
2630
27-
# Required for Google KMS
31+
# Required for Google Cloud KMS
2832
GOOGLE_APPLICATION_PROJECT_ID=<google_project_id>
2933
GOOGLE_KMS_KEY_RING_ID=<key_ring_id>
3034
GOOGLE_KMS_LOCATION_ID=<location_of_key_ring>
31-
GOOGLE_KMS_CRYPTO_KEY_ID=<kms_key_id> # If created on Google Console
3235
```

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
"docker": "docker compose --env-file ./.env up --remove-orphans",
1111
"docker:build": "docker compose build --no-cache",
1212
"dev": "yarn dev:infra && yarn dev:server & sleep 10 && yarn dev:worker",
13-
"dev:server": "nodemon --watch 'server/**/*.ts' --watch 'core/**/*.ts' --exec 'npx tsx ./server/index.ts'",
14-
"dev:worker": "nodemon --watch 'worker/**/*.ts' --watch 'core/**/*.ts' --exec 'npx tsx ./worker/index.ts'",
13+
"dev:server": "nodemon --watch 'server/**/*.ts' --watch 'core/**/*.ts' --exec 'npx tsx ./server/index.ts' --files server/index.ts",
14+
"dev:worker": "nodemon --watch 'worker/**/*.ts' --watch 'core/**/*.ts' --exec 'npx tsx ./worker/index.ts' --files worker/index.ts",
1515
"dev:infra": "docker compose -f ./docker-compose-infra.yml up -d",
1616
"build": "yarn && rm -rf dist && tsc -p ./tsconfig.json --outDir dist",
1717
"start": "yarn start:server & sleep 20 && yarn start:worker",
@@ -36,6 +36,7 @@
3636
"@fastify/type-provider-typebox": "^3.2.0",
3737
"@fastify/websocket": "^8.2.0",
3838
"@google-cloud/kms": "^4.0.0",
39+
"@prisma/client": "5.2.0",
3940
"@sinclair/typebox": "^0.28",
4041
"@t3-oss/env-core": "^0.6.0",
4142
"@thirdweb-dev/chains": "^0.1.46",
@@ -57,6 +58,7 @@
5758
"p-queue": "^7.3.4",
5859
"pg": "^8.11.0",
5960
"pino-pretty": "^10.0.0",
61+
"prisma": "^5.2.0",
6062
"uuidv4": "^6.2.13",
6163
"zod": "^3.21.4"
6264
},
@@ -91,5 +93,8 @@
9193
"lint-staged": {
9294
"*.{js,ts}": "eslint --cache --fix",
9395
"*.{js,ts,md}": "prettier --write"
96+
},
97+
"prisma": {
98+
"schema": "./src/prisma/schema.prisma"
9499
}
95100
}

server/helpers/openapi.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ import { env } from "../../core";
77
// to be separate unlike old implementation
88

99
export const openapi = async (server: FastifyInstance) => {
10-
if (process.env.NODE_ENV === "production") {
11-
return Promise.resolve();
12-
}
1310
await server.register(swagger, {
1411
mode: "dynamic",
1512
openapi: {

server/helpers/server.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ const createServer = async (serverName: string): Promise<FastifyInstance> => {
2626
);
2727
}
2828

29+
if (process.env.NODE_ENV === "production") {
30+
if (request.routerPath?.includes("static")) {
31+
return reply.status(404).send({
32+
statusCode: 404,
33+
error: "Not Found",
34+
message: "Not Found",
35+
});
36+
}
37+
}
38+
2939
const { url } = request;
3040
// Skip Authentication for Health Check and Static Files and JSON Files for Swagger
3141
// Doing Auth check onRequest helps prevent unauthenticated requests from consuming server resources.

src/db/client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { PrismaClient } from "@prisma/client";
2+
3+
export const prisma = new PrismaClient();

src/db/transactions.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { DeployTransaction, Transaction } from "@thirdweb-dev/sdk";
2+
import { prisma } from "./client";
3+
// TODO: Don't use BigNumber here - SDK should handle this
4+
import { BigNumber } from "ethers";
5+
import { env } from "../../core";
6+
import { TransactionStatusEnum } from "../../server/schemas/transaction";
7+
8+
export const getTxById = async (queueId: string) => {
9+
const tx = await prisma.transactions.findUnique({
10+
where: {
11+
id: queueId,
12+
},
13+
});
14+
15+
if (!tx) {
16+
// TODO: Defined error types
17+
throw new Error(`Transaction with ID ${queueId} not found!`);
18+
}
19+
20+
return tx;
21+
};
22+
23+
export const getQueuedTxs = async (): Promise<Transaction[]> => {
24+
// TODO: Don't use env var for transactions to batch
25+
return prisma.$queryRaw`
26+
SELECT
27+
*
28+
FROM
29+
"transactions"
30+
WHERE
31+
"processedAt" IS NULL
32+
AND "sentAt" IS NULL
33+
AND "minedAt" IS NULL
34+
ORDER BY
35+
"queuedAt"
36+
LIMIT
37+
${env.TRANSACTIONS_TO_BATCH}
38+
FOR UPDATE SKIP LOCKED
39+
`;
40+
};
41+
42+
export const getSentTxs = async () => {
43+
return prisma.transactions.findMany({
44+
where: {
45+
processedAt: {
46+
not: null,
47+
},
48+
sentAt: {
49+
not: null,
50+
},
51+
transactionHash: {
52+
not: null,
53+
},
54+
minedAt: null,
55+
errorMessage: null,
56+
retryCount: {
57+
// TODO: What should the max retries be here?
58+
lt: 3,
59+
},
60+
},
61+
orderBy: [
62+
{
63+
sentAt: "asc",
64+
},
65+
],
66+
// TODO: Should this be coming from env?
67+
take: env.MIN_TX_TO_CHECK_FOR_MINED_STATUS,
68+
});
69+
};
70+
71+
export const getAllTxs = async (
72+
page: number,
73+
limit: number,
74+
// TODO: Replace this...
75+
filter?: TransactionStatusEnum,
76+
contractExtensions?: string[],
77+
) => {
78+
let filterBy:
79+
| "queuedAt"
80+
| "sentAt"
81+
| "processedAt"
82+
| "minedAt"
83+
| "errorMessage"
84+
| undefined;
85+
86+
if (filter === TransactionStatusEnum.Queued) {
87+
filterBy = "queuedAt";
88+
} else if (filter === TransactionStatusEnum.Submitted) {
89+
filterBy = "sentAt";
90+
} else if (filter === TransactionStatusEnum.Processed) {
91+
filterBy = "processedAt";
92+
} else if (filter === TransactionStatusEnum.Mined) {
93+
filterBy = "minedAt";
94+
} else if (filter === TransactionStatusEnum.Errored) {
95+
filterBy = "errorMessage";
96+
}
97+
98+
return prisma.transactions.findMany({
99+
where: {
100+
...(filterBy
101+
? {
102+
[filterBy]: {
103+
not: null,
104+
},
105+
}
106+
: {}),
107+
...(contractExtensions
108+
? {
109+
contractExtension: {
110+
in: contractExtensions,
111+
},
112+
}
113+
: {}),
114+
},
115+
orderBy: [
116+
{
117+
queuedAt: "desc",
118+
},
119+
],
120+
skip: (page - 1) * limit,
121+
take: limit,
122+
});
123+
};
124+
125+
// TODO: Simulation should be done before this function...
126+
export const queueTx = async (
127+
chainId: number,
128+
tx: Transaction | DeployTransaction,
129+
// TODO: Clean up how extension is passed in
130+
contractExtension: string,
131+
) => {
132+
// TODO: SDK should have a JSON.stringify() method.
133+
const fromAddress = (await tx.getSignerAddress()).toLowerCase();
134+
const toAddress = tx.getTarget().toLowerCase();
135+
const data = tx.encode();
136+
const functionName = tx.getMethod();
137+
const functionArgs = tx.getArgs().toString();
138+
const value = BigNumber.from(await tx.getValue()).toHexString();
139+
140+
// TODO: Should we call this txId so it's easier to spell?
141+
const { id: queueId } = await prisma.transactions.create({
142+
data: {
143+
chainId,
144+
fromAddress,
145+
toAddress,
146+
data,
147+
value,
148+
functionName,
149+
functionArgs,
150+
contractExtension,
151+
},
152+
});
153+
154+
return queueId;
155+
};
156+
157+
// TODO: Switch all functions to object params
158+
export const retryTx = async (
159+
queuedId: string,
160+
maxFeePerGas: string,
161+
maxPriorityFeePerGas: string,
162+
) => {
163+
await prisma.transactions.update({
164+
where: {
165+
id: queuedId,
166+
},
167+
// TODO: Do these need to all be separate fields?
168+
data: {
169+
retryMaxFeePerGas: maxFeePerGas,
170+
retryMaxPriorityFeePerGas: maxPriorityFeePerGas,
171+
},
172+
});
173+
};

src/db/wallets.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { WalletDetails } from "@prisma/client";
2+
import { prisma } from "./client";
3+
4+
// TODO: Add error logging handler from req.log to all queries
5+
export const getAllWallets = async (chainId: number) => {
6+
return prisma.walletNonce.findMany({
7+
where: {
8+
chainId,
9+
},
10+
});
11+
};
12+
13+
export const createWalletDetails = async (walletDetails: WalletDetails) => {
14+
return prisma.walletDetails.create({
15+
data: walletDetails,
16+
});
17+
};
18+
19+
/*
20+
export const createWalletNonce = async (
21+
chainId: number,
22+
walletAddress: string,
23+
) => {
24+
// TODO: Merge with furqan's branch...
25+
const sdk = await getSDK(chainId);
26+
// TODO: Replace BigNumber
27+
const nonce = BigNumber.from(
28+
(await getWalletNonce(walletAddress.toLowerCase(), sdk.getProvider())) ?? 0,
29+
).toNumber();
30+
31+
return prisma.walletNonce.create({
32+
data: {
33+
chainId,
34+
address: walletAddress,
35+
nonce,
36+
},
37+
});
38+
};
39+
*/
40+
41+
export const updateWalletNonce = async (
42+
chainId: number,
43+
walletAddress: string,
44+
nonce: number,
45+
) => {
46+
await prisma.walletNonce.update({
47+
where: {
48+
address_chainId: {
49+
address: walletAddress,
50+
chainId,
51+
},
52+
},
53+
data: {
54+
nonce,
55+
},
56+
});
57+
};

0 commit comments

Comments
 (0)