Skip to content

Commit c791e06

Browse files
authored
Merge branch 'main' into pb/aws-kms-v5
Signed-off-by: Prithvish Baidya <[email protected]>
2 parents 0a07a60 + 3015e63 commit c791e06

39 files changed

+665
-589
lines changed

.github/workflows/check-build.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Check Build
2+
3+
on: pull_request
4+
5+
jobs:
6+
build:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- name: Checkout code
10+
uses: actions/checkout@v3
11+
with:
12+
ref: ${{ github.ref }}
13+
14+
- name: Set up Node.js
15+
uses: actions/setup-node@v3
16+
with:
17+
node-version: "18"
18+
cache: "yarn"
19+
20+
- name: Install dependencies
21+
run: yarn install
22+
23+
- name: Run build
24+
run: yarn build

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Engine is an open-source, backend HTTP server that provides a production-ready i
3333
- 700+ transactions sent onchain per second (can fill up an EVM block!)
3434
- Managed backend wallets (local, KMS)
3535
- Contract calls and deployments ([all EVM blockchains](https://thirdweb.com/chainlist) + private subnets)
36-
- Gas optimizations with retries
36+
- Gas-optimized retries, gas ceilings, and timeouts
3737
- Smart account support with session tokens
3838
- Gasless transactions with relayers
3939
- Wallet and contract webhooks

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"generate:sdk": "npx tsx ./src/scripts/generate-sdk && cd ./sdk && yarn build",
1717
"prisma:setup:dev": "npx tsx ./src/scripts/setup-db.ts",
1818
"prisma:setup:prod": "npx tsx ./dist/scripts/setup-db.js",
19-
"start": "yarn prisma:setup:prod && yarn start:run",
19+
"start": "yarn prisma:setup:prod && yarn start:migrations && yarn start:run",
20+
"start:migrations": "npx tsx ./dist/scripts/apply-migrations.js",
2021
"start:run": "node --experimental-specifier-resolution=node ./dist/index.js",
2122
"start:docker": "docker compose --profile engine --env-file ./.env up --remove-orphans",
2223
"start:docker-force-build": "docker compose --profile engine --env-file ./.env up --remove-orphans --build",

src/db/transactions/db.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import superjson from "superjson";
2-
import { env } from "../../utils/env";
3-
import { redis } from "../../utils/redis/redis";
4-
import { AnyTransaction } from "../../utils/transaction/types";
2+
import { MAX_REDIS_BATCH_SIZE, redis } from "../../utils/redis/redis";
3+
import type { AnyTransaction } from "../../utils/transaction/types";
54

65
/**
76
* Schemas
@@ -111,13 +110,9 @@ export class TransactionDB {
111110
}
112111

113112
const result: AnyTransaction[] = [];
114-
for (
115-
let i = 0;
116-
i < queueIds.length;
117-
i += env.__EXPERIMENTAL_REDIS_BATCH_SIZE
118-
) {
113+
for (let i = 0; i < queueIds.length; i += MAX_REDIS_BATCH_SIZE) {
119114
const keys = queueIds
120-
.slice(i, i + env.__EXPERIMENTAL_REDIS_BATCH_SIZE)
115+
.slice(i, i + MAX_REDIS_BATCH_SIZE)
121116
.map(this.transactionDetailsKey);
122117
const vals = await redis.mget(...keys);
123118

@@ -141,13 +136,9 @@ export class TransactionDB {
141136
}
142137

143138
let numDeleted = 0;
144-
for (
145-
let i = 0;
146-
i < queueIds.length;
147-
i += env.__EXPERIMENTAL_REDIS_BATCH_SIZE
148-
) {
139+
for (let i = 0; i < queueIds.length; i += MAX_REDIS_BATCH_SIZE) {
149140
const keys = queueIds
150-
.slice(i, i + env.__EXPERIMENTAL_REDIS_BATCH_SIZE)
141+
.slice(i, i + MAX_REDIS_BATCH_SIZE)
151142
.map(this.transactionDetailsKey);
152143
numDeleted += await redis.unlink(...keys);
153144
}

src/db/transactions/queueTx.ts

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { DeployTransaction, Transaction } from "@thirdweb-dev/sdk";
2-
import { ERC4337EthersSigner } from "@thirdweb-dev/wallets/dist/declarations/src/evm/connectors/smart-wallet/lib/erc4337-signer";
3-
import { Address, ZERO_ADDRESS } from "thirdweb";
2+
import type { ERC4337EthersSigner } from "@thirdweb-dev/wallets/dist/declarations/src/evm/connectors/smart-wallet/lib/erc4337-signer";
3+
import { ZERO_ADDRESS, type Address } from "thirdweb";
44
import type { ContractExtension } from "../../schema/extension";
5+
import { parseTransactionOverrides } from "../../server/utils/transactionOverrides";
56
import { maybeBigInt, normalizeAddress } from "../../utils/primitiveTypes";
67
import { insertTransaction } from "../../utils/transaction/insertTransaction";
8+
import type { InsertedTransaction } from "../../utils/transaction/types";
79

810
interface QueueTxParams {
911
// we should move away from Transaction type (v4 SDK)
@@ -49,10 +51,8 @@ export const queueTx = async ({
4951
functionName,
5052
functionArgs,
5153
extension,
52-
gas: maybeBigInt(txOverrides?.gas),
53-
maxFeePerGas: maybeBigInt(txOverrides?.maxFeePerGas),
54-
maxPriorityFeePerGas: maybeBigInt(txOverrides?.maxPriorityFeePerGas),
55-
};
54+
...parseTransactionOverrides(txOverrides),
55+
} satisfies Partial<InsertedTransaction>;
5656

5757
// TODO: We need a much safer way of detecting if the transaction should be a user operation
5858
const isUserOp = !!(tx.getSigner as ERC4337EthersSigner).erc4337provider;
@@ -76,27 +76,25 @@ export const queueTx = async ({
7676
idempotencyKey,
7777
shouldSimulate: simulateTx,
7878
});
79-
} else {
80-
const isPublishedContractDeploy =
81-
tx.getTarget() === ZERO_ADDRESS &&
82-
functionName === "deploy" &&
83-
extension === "deploy-published";
79+
}
8480

85-
const queueId = await insertTransaction({
86-
insertedTransaction: {
87-
...baseTransaction,
88-
isUserOp: false,
89-
deployedContractAddress,
90-
deployedContractType,
91-
from: normalizeAddress(await tx.getSignerAddress()),
92-
to: normalizeAddress(
93-
isPublishedContractDeploy ? undefined : tx.getTarget(),
94-
),
95-
},
96-
idempotencyKey,
97-
shouldSimulate: simulateTx,
98-
});
81+
const isPublishedContractDeploy =
82+
tx.getTarget() === ZERO_ADDRESS &&
83+
functionName === "deploy" &&
84+
extension === "deploy-published";
9985

100-
return queueId;
101-
}
86+
return await insertTransaction({
87+
insertedTransaction: {
88+
...baseTransaction,
89+
isUserOp: false,
90+
deployedContractAddress,
91+
deployedContractType,
92+
from: normalizeAddress(await tx.getSignerAddress()),
93+
to: normalizeAddress(
94+
isPublishedContractDeploy ? undefined : tx.getTarget(),
95+
),
96+
},
97+
idempotencyKey,
98+
shouldSimulate: simulateTx,
99+
});
102100
};

src/db/wallets/walletNonce.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {
2-
Address,
32
eth_getTransactionCount,
43
getAddress,
54
getRpcClient,
5+
type Address,
66
} from "thirdweb";
77
import { getChain } from "../../utils/chain";
88
import { logger } from "../../utils/logger";
@@ -37,7 +37,7 @@ export const getUsedBackendWallets = async (
3737
return keys.map((key) => {
3838
const tokens = key.split(":");
3939
return {
40-
chainId: parseInt(tokens[1]),
40+
chainId: Number.parseInt(tokens[1]),
4141
walletAddress: getAddress(tokens[2]),
4242
};
4343
});
@@ -61,7 +61,7 @@ export const lastUsedNonceKey = (chainId: number, walletAddress: Address) =>
6161
export const splitLastUsedNonceKey = (key: string) => {
6262
const _splittedKeys = key.split(":");
6363
const walletAddress = normalizeAddress(_splittedKeys[2]);
64-
const chainId = parseInt(_splittedKeys[1]);
64+
const chainId = Number.parseInt(_splittedKeys[1]);
6565
return { walletAddress, chainId };
6666
};
6767

@@ -87,7 +87,7 @@ export const sentNoncesKey = (chainId: number, walletAddress: Address) =>
8787
export const splitSentNoncesKey = (key: string) => {
8888
const _splittedKeys = key.split(":");
8989
const walletAddress = normalizeAddress(_splittedKeys[2]);
90-
const chainId = parseInt(_splittedKeys[1]);
90+
const chainId = Number.parseInt(_splittedKeys[1]);
9191
return { walletAddress, chainId };
9292
};
9393

@@ -166,7 +166,7 @@ export const acquireNonce = async (args: {
166166
nonce,
167167
queueId,
168168
});
169-
return { nonce, isRecycledNonce: false };
169+
return { nonce, isRecycledNonce };
170170
};
171171

172172
/**
@@ -181,7 +181,7 @@ export const recycleNonce = async (
181181
walletAddress: Address,
182182
nonce: number,
183183
) => {
184-
if (isNaN(nonce)) {
184+
if (Number.isNaN(nonce)) {
185185
logger({
186186
level: "warn",
187187
message: `[recycleNonce] Invalid nonce: ${nonce}`,
@@ -209,7 +209,7 @@ const _acquireRecycledNonce = async (
209209
if (result.length === 0) {
210210
return null;
211211
}
212-
return parseInt(result[0]);
212+
return Number.parseInt(result[0]);
213213
};
214214

215215
/**
@@ -246,7 +246,7 @@ export const syncLatestNonceFromOnchain = async (
246246
export const inspectNonce = async (chainId: number, walletAddress: Address) => {
247247
const key = lastUsedNonceKey(chainId, walletAddress);
248248
const nonce = await redis.get(key);
249-
return nonce ? parseInt(nonce) : 0;
249+
return nonce ? Number.parseInt(nonce) : 0;
250250
};
251251

252252
/**

src/scripts/apply-migrations.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { logger } from "../utils/logger";
2+
import { acquireLock, releaseLock, waitForLock } from "../utils/redis/lock";
3+
import { redis } from "../utils/redis/redis";
4+
5+
const MIGRATION_LOCK_TTL_SECONDS = 60;
6+
7+
const main = async () => {
8+
// Acquire a lock to allow only one host to run migrations.
9+
// Other hosts block until the migration is completed or lock times out.
10+
const acquiredLock = await acquireLock(
11+
"lock:apply-migrations",
12+
MIGRATION_LOCK_TTL_SECONDS,
13+
);
14+
if (!acquiredLock) {
15+
logger({
16+
level: "info",
17+
message: "Migration in progress. Waiting for the lock to release...",
18+
service: "server",
19+
});
20+
await waitForLock("lock:apply-migrations");
21+
process.exit(0);
22+
}
23+
24+
try {
25+
await migrateRecycledNonces();
26+
27+
logger({
28+
level: "info",
29+
message: "Completed migrations successfully.",
30+
service: "server",
31+
});
32+
} catch (e) {
33+
logger({
34+
level: "error",
35+
message: `Failed to complete migrations: ${e}`,
36+
service: "server",
37+
});
38+
process.exit(1);
39+
} finally {
40+
await releaseLock("lock:apply-migrations");
41+
}
42+
43+
process.exit(0);
44+
};
45+
46+
const migrateRecycledNonces = async () => {
47+
const keys = await redis.keys("nonce-recycled:*");
48+
49+
// For each `nonce-recycled:*` key that is a "set" in Redis,
50+
// migrate all members to a sorted set with score == nonce.
51+
for (const key of keys) {
52+
const type = await redis.type(key);
53+
if (type !== "set") {
54+
continue;
55+
}
56+
57+
const members = await redis.smembers(key);
58+
await redis.del(key);
59+
if (members.length > 0) {
60+
const args = members.flatMap((member) => {
61+
const score = Number.parseInt(member);
62+
return Number.isNaN(score) ? [] : [score, member];
63+
});
64+
await redis.zadd(key, ...args);
65+
}
66+
}
67+
};
68+
69+
main();

src/server/index.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,23 @@ export const initServer = async () => {
5050
};
5151
}
5252

53+
// env.TRUST_PROXY is used to determine if the X-Forwarded-For header should be trusted.
54+
// This option is force enabled for cloud-hosted Engines.
55+
// See: https://fastify.dev/docs/latest/Reference/Server/#trustproxy
56+
const trustProxy = env.TRUST_PROXY || !!env.ENGINE_TIER;
57+
if (trustProxy) {
58+
logger({
59+
service: "server",
60+
level: "info",
61+
message: "Server is enabled with trustProxy.",
62+
});
63+
}
64+
5365
// Start the server with middleware.
5466
const server: FastifyInstance = fastify({
5567
connectionTimeout: SERVER_CONNECTION_TIMEOUT,
5668
disableRequestLogging: true,
57-
// env.TRUST_PROXY is used to determine if the X-Forwarded-For header should be trusted.
58-
// See: https://fastify.dev/docs/latest/Reference/Server/#trustproxy
59-
trustProxy: env.TRUST_PROXY,
69+
trustProxy,
6070
...(env.ENABLE_HTTPS ? httpsObject : {}),
6171
}).withTypeProvider<TypeBoxTypeProvider>();
6272

@@ -97,7 +107,7 @@ export const initServer = async () => {
97107
logger({
98108
service: "server",
99109
level: "fatal",
100-
message: `Failed to start server`,
110+
message: "Failed to start server",
101111
error: err,
102112
});
103113
process.exit(1);

0 commit comments

Comments
 (0)