Skip to content

Commit 785ce90

Browse files
authored
Transaction Retry Implementation (#111)
* Adding retry logic. ToDo * Added retry logic. Need to cleanup logs. First draft * commented out erroring tx when retrying * updated SQL for new columns updated retry logic * added retry end-point with overrides gas values as input for retry * updated processTransaction to remove retry code * updated retry logic * updated logs * updated retry end-point to reset number of retries * updated min block to wait before retry to 50 & Max gas values to 55gwei * Added process exit if init scripts fail & added ability to create Database on psql * added ensureDatabaseExists as a startup check * Added wallet-type local to return when no ppk/gcp/aws is used * updated import * handled walletType for import. Local not supported for now * added ssl rejectUnauthorized, from https://node-postgres.com/announcements#2020-02-25 * updates for ensureDatabaseExists * hack. need to change this
1 parent 4132d03 commit 785ce90

26 files changed

+556
-173
lines changed

.env.example

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,15 @@ BENCHMARK_POST_BODY='{
6868
"args": ["0x1946267d81Fb8aDeeEa28e6B98bcD446c8248473", 100000]
6969
}'
7070
BENCHMARK_CONCURRENCY=10
71-
BENCHMARK_REQUESTS=10
71+
BENCHMARK_REQUESTS=10
72+
73+
# Retey Gas Max Values (All in wei)
74+
# Default Values
75+
# MAX_FEE_PER_GAS_FOR_RETRY=55000000000 (55 Gwei)
76+
# MAX_PRIORITY_FEE_PER_GAS_FOR_RETRY=55000000000 (55 Gwei)
77+
# MAX_RETRIES_FOR_TX=3
78+
# MAX_BLOCKS_ELAPSED_BEFORE_RETRY=50
79+
# RETRY_TX_CRON_SCHEDULE=*/30 * * * * *
80+
# RETRY_TX_ENABLED=true
81+
MAX_FEE_PER_GAS_FOR_RETRY=55000000000
82+
MAX_PRIORITY_FEE_PER_GAS_FOR_RETRY=55000000000

core/database/dbConnect.ts

Lines changed: 8 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,24 @@
1-
import { FastifyInstance, FastifyRequest } from "fastify";
21
import pg, { Knex } from "knex";
32
import { env } from "../env";
43

54
// Defaults to postgres
65
const dbClient = env.DATABASE_CLIENT;
76
const connectionString = env.POSTGRES_CONNECTION_URL;
87

9-
const DATABASE_NAME =
10-
new URL(connectionString).pathname.split("/")[1] || "postgres";
11-
12-
export const connectToDB = async (
13-
server: FastifyInstance | FastifyRequest,
8+
export const connectToDatabase = async (
9+
databaseURL?: string,
1410
): Promise<Knex> => {
15-
// Creating KNEX Config
16-
1711
let knexConfig: Knex.Config = {
1812
client: dbClient,
19-
connection: connectionString,
13+
connection: {
14+
connectionString: databaseURL || connectionString,
15+
ssl: {
16+
rejectUnauthorized: false,
17+
},
18+
},
2019
acquireConnectionTimeout: 10000,
2120
};
2221

23-
// Set the appropriate databse client package
24-
let dbClientPackage: any;
25-
switch (dbClient) {
26-
case "pg":
27-
dbClientPackage = pg;
28-
break;
29-
default:
30-
throw new Error(`Unsupported database client: ${dbClient}`);
31-
}
32-
33-
let knex = dbClientPackage(knexConfig);
34-
35-
// Check if Database Exists & create if it doesn't
36-
try {
37-
let hasDatabase: any;
38-
switch (dbClient) {
39-
case "pg":
40-
server.log.debug("checking if pg database exists");
41-
hasDatabase = await knex.raw(
42-
`SELECT 1 from pg_database WHERE datname = '${DATABASE_NAME}'`,
43-
);
44-
server.log.info(`CHECKING for Database ${DATABASE_NAME}...`);
45-
if (!hasDatabase.rows.length) {
46-
await knex.raw(`CREATE DATABASE ${DATABASE_NAME}`);
47-
} else {
48-
server.log.info(`Database ${DATABASE_NAME} already exists`);
49-
}
50-
break;
51-
default:
52-
throw new Error(
53-
`Unsupported database client: ${dbClient}. Cannot create database ${DATABASE_NAME}`,
54-
);
55-
}
56-
} catch (err) {
57-
server.log.error(err);
58-
throw new Error(`Error creating database ${DATABASE_NAME}`);
59-
}
60-
61-
// Updating knex Config
62-
knexConfig = {
63-
client: dbClient,
64-
connection: connectionString,
65-
};
66-
67-
await knex.destroy();
68-
// re-instantiate connection with new config
69-
knex = dbClientPackage(knexConfig);
70-
71-
return knex;
72-
};
73-
74-
export const connectWithDatabase = async (): Promise<Knex> => {
75-
let knexConfig: Knex.Config = {
76-
client: dbClient,
77-
connection: connectionString,
78-
};
79-
8022
// Set the appropriate databse client package
8123
let dbClientPackage: typeof pg;
8224
switch (dbClient) {

core/database/dbOperation.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import { getSupportedChains } from "@thirdweb-dev/sdk";
33
import { BigNumber } from "ethers";
44
import { FastifyInstance } from "fastify";
55
import { Knex } from "knex";
6-
import { getInstanceAdminWalletType, getWalletBackUpType } from "../helpers";
6+
import { getWalletType } from "../helpers";
77
import { WalletData } from "../interfaces";
88
import { getSDK } from "../sdk/sdk";
99
import { getWalletNonce } from "../services/blockchain";
10-
import { connectWithDatabase } from "./dbConnect";
10+
import { connectToDatabase } from "./dbConnect";
1111

1212
interface WalletExtraData {
1313
awsKmsKeyId?: string;
@@ -40,7 +40,7 @@ export const getWalletDetails = async (
4040
let passedAsParameter = true;
4141
if (!database) {
4242
passedAsParameter = false;
43-
database = await connectWithDatabase();
43+
database = await connectToDatabase();
4444
}
4545
const walletDetails = await database("wallets")
4646
.select("*")
@@ -103,9 +103,7 @@ export const addWalletDataWithSupportChainsNonceToDB = async (
103103
let lastUsedNonce = -1;
104104
let walletType = extraTableData?.walletType
105105
? extraTableData?.walletType
106-
: isWeb3APIInitWallet
107-
? getInstanceAdminWalletType()
108-
: getWalletBackUpType();
106+
: getWalletType();
109107
const sdk = await getSDK(slug, {
110108
walletAddress,
111109
walletType,

core/database/dbPrereqs.ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,25 @@ import { promises as fs } from "fs";
22
import { dirname } from "path";
33
import { fileURLToPath } from "url";
44

5-
import { FastifyInstance } from "fastify";
5+
import { FastifyInstance, FastifyRequest } from "fastify";
66
import { StatusCodes } from "http-status-codes";
77
import { env } from "../env";
88
import { createCustomError } from "../error/customError";
9-
import { connectToDB, connectWithDatabase } from "./dbConnect";
9+
import { connectToDatabase } from "./dbConnect";
1010

1111
const __filename = fileURLToPath(import.meta.url);
1212
const __dirname = dirname(__filename);
13+
const dbClient = env.DATABASE_CLIENT;
14+
const connectionString = env.POSTGRES_CONNECTION_URL;
15+
16+
const DATABASE_NAME =
17+
new URL(connectionString).pathname.split("/")[1] || "postgres";
1318

1419
export const checkTablesExistence = async (
1520
server: FastifyInstance,
1621
): Promise<void> => {
1722
try {
18-
const knex = await connectToDB(server);
23+
const knex = await connectToDatabase();
1924
// Check if the tables Exists
2025
const tablesList: string[] = env.DB_TABLES_LIST.split(",").map(function (
2126
item: any,
@@ -63,7 +68,7 @@ export const implementTriggerOnStartUp = async (
6368
): Promise<void> => {
6469
try {
6570
// Connect to the DB
66-
const knex = await connectWithDatabase();
71+
const knex = await connectToDatabase();
6772

6873
const triggersList: string[] = env.DB_TRIGGERS_LIST.split(",").map(
6974
function (item: any) {
@@ -113,3 +118,49 @@ export const implementTriggerOnStartUp = async (
113118
throw customError;
114119
}
115120
};
121+
122+
export const ensureDatabaseExists = async (
123+
server: FastifyInstance | FastifyRequest,
124+
): Promise<void> => {
125+
try {
126+
// Creating KNEX Config
127+
let modifiedConnectionString = connectionString;
128+
if (DATABASE_NAME !== "postgres") {
129+
// This is required if the Database mentioned in the connection string is not postgres
130+
// as we need to connect to the postgres database to create the user provied database
131+
// and then connect to the user provided database
132+
modifiedConnectionString = connectionString.replace(
133+
`/${DATABASE_NAME}`,
134+
"/postgres",
135+
);
136+
}
137+
138+
let knex = await connectToDatabase(modifiedConnectionString);
139+
140+
// Check if Database Exists & create if it doesn't
141+
let hasDatabase: any;
142+
switch (dbClient) {
143+
case "pg":
144+
server.log.debug("checking if pg database exists");
145+
hasDatabase = await knex.raw(
146+
`SELECT 1 from pg_database WHERE datname = '${DATABASE_NAME}'`,
147+
);
148+
server.log.info(`CHECKING for Database ${DATABASE_NAME}...`);
149+
if (!hasDatabase.rows.length) {
150+
await knex.raw(`CREATE DATABASE ${DATABASE_NAME}`);
151+
} else {
152+
server.log.info(`Database ${DATABASE_NAME} already exists`);
153+
}
154+
break;
155+
default:
156+
throw new Error(
157+
`Unsupported database client: ${dbClient}. Cannot create database ${DATABASE_NAME}`,
158+
);
159+
}
160+
161+
await knex.destroy();
162+
} catch (error) {
163+
server.log.error(error);
164+
throw new Error(`Error creating database ${DATABASE_NAME}`);
165+
}
166+
};

core/database/sql-schemas/transactions.sql

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ CREATE TABLE IF NOT EXISTS transactions (
2626
"maxFeePerGas" VARCHAR(255),
2727
"txValue" VARCHAR(100),
2828
"txMinedTimestamp" TIMESTAMP,
29-
"blockNumber" BIGINT
29+
"blockNumber" BIGINT,
30+
"numberOfRetries" INTEGER DEFAULT 0,
31+
"overrideGasValuesForTx" BOOLEAN DEFAULT FALSE,
32+
"txSubmittedAtBlockNumber" BIGINT,
33+
"overrideMaxPriorityFeePerGas" VARCHAR(255),
34+
"overrideMaxFeePerGas" VARCHAR(255)
3035
);
3136

3237
ALTER TABLE transactions
@@ -46,4 +51,9 @@ ADD COLUMN IF NOT EXISTS "contractType" VARCHAR(255),
4651
ADD COLUMN IF NOT EXISTS "errorMessage" TEXT DEFAULT NULL,
4752
ADD COLUMN IF NOT EXISTS "txValue" VARCHAR(100) DEFAULT NULL,
4853
ADD COLUMN IF NOT EXISTS "txMinedTimestamp" TIMESTAMP,
49-
ADD COLUMN IF NOT EXISTS "blockNumber" BIGINT;
54+
ADD COLUMN IF NOT EXISTS "blockNumber" BIGINT,
55+
ADD COLUMN IF NOT EXISTS "numberOfRetries" INTEGER DEFAULT 0,
56+
ADD COLUMN IF NOT EXISTS "overrideGasValuesForTx" BOOLEAN DEFAULT FALSE,
57+
ADD COLUMN IF NOT EXISTS "txSubmittedAtBlockNumber" BIGINT,
58+
ADD COLUMN IF NOT EXISTS "overrideMaxPriorityFeePerGas" VARCHAR(255),
59+
ADD COLUMN IF NOT EXISTS "overrideMaxFeePerGas" VARCHAR(255);

core/env.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ export const env = createEnv({
5757
GOOGLE_KMS_KEY_VERSION_ID: z.string().min(1).optional(),
5858
GOOGLE_APPLICATION_CREDENTIAL_EMAIL: z.string().min(1).optional(),
5959
GOOGLE_APPLICATION_CREDENTIAL_PRIVATE_KEY: z.string().min(1).optional(),
60+
RETRY_TX_ENABLED: boolSchema("true"),
61+
MAX_FEE_PER_GAS_FOR_RETRY: z.string().default("55000000000"),
62+
MAX_PRIORITY_FEE_PER_GAS_FOR_RETRY: z.string().default("55000000000"),
63+
MAX_RETRIES_FOR_TX: z.coerce.number().default(3),
64+
RETRY_TX_CRON_SCHEDULE: z.string().default("*/30 * * * * *"),
65+
MAX_BLOCKS_ELAPSED_BEFORE_RETRY: z.coerce.number().default(50),
66+
MAX_WAIT_TIME_BEFORE_RETRY: z.coerce.number().default(600),
6067
},
6168
clientPrefix: "NEVER_USED",
6269
client: {},
@@ -94,6 +101,15 @@ export const env = createEnv({
94101
process.env.GOOGLE_APPLICATION_CREDENTIAL_EMAIL,
95102
GOOGLE_APPLICATION_CREDENTIAL_PRIVATE_KEY:
96103
process.env.GOOGLE_APPLICATION_CREDENTIAL_PRIVATE_KEY,
104+
RETRY_TX_ENABLED: process.env.RETRY_TX_ENABLED,
105+
MAX_FEE_PER_GAS_FOR_RETRY: process.env.MAX_FEE_PER_GAS_FOR_RETRY,
106+
MAX_PRIORITY_FEE_PER_GAS_FOR_RETRY:
107+
process.env.MAX_PRIORITY_FEE_PER_GAS_FOR_RETRY,
108+
MAX_RETRIES_FOR_TX: process.env.MAX_RETRIES_FOR_TX,
109+
RETRY_TX_CRON_SCHEDULE: process.env.RETRY_TX_CRON_SCHEDULE,
110+
MAX_BLOCKS_ELAPSED_BEFORE_RETRY:
111+
process.env.MAX_BLOCKS_ELAPSED_BEFORE_RETRY,
112+
MAX_WAIT_TIME_BEFORE_RETRY: process.env.MAX_WAIT_TIME_BEFORE_RETRY,
97113
},
98114
onValidationError: (error: ZodError) => {
99115
console.error(

core/helpers/index.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const isValidHttpUrl = (urlString: string): boolean => {
1818
return url.protocol === "http:" || url.protocol === "https:";
1919
};
2020

21-
export const getInstanceAdminWalletType = (): string => {
21+
export const getWalletType = (): string => {
2222
if (WALLET_PRIVATE_KEY) {
2323
return "ppk";
2424
}
@@ -27,15 +27,6 @@ export const getInstanceAdminWalletType = (): string => {
2727
return "aws_kms";
2828
}
2929

30-
// ToDo GCP KMS
31-
return "gcp_kms";
32-
};
33-
34-
export const getWalletBackUpType = (): string => {
35-
if (AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY && AWS_REGION) {
36-
return "aws_kms";
37-
}
38-
3930
if (
4031
env.GOOGLE_APPLICATION_CREDENTIAL_EMAIL &&
4132
env.GOOGLE_APPLICATION_CREDENTIAL_PRIVATE_KEY &&
@@ -44,9 +35,9 @@ export const getWalletBackUpType = (): string => {
4435
env.GOOGLE_KMS_KEY_VERSION_ID &&
4536
env.GOOGLE_KMS_LOCATION_ID
4637
) {
47-
return "gcp_kms";
38+
return "aws_kms";
4839
}
4940

5041
// ToDo GCP KMS
51-
return "ppk";
42+
return "local";
5243
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"dev:worker": "nodemon --watch 'worker/**/*.ts' --watch 'core/**/*.ts' --exec 'npx tsx ./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",
17-
"start": "yarn start:server & sleep 10 && yarn start:worker",
17+
"start": "yarn start:server & sleep 20 && yarn start:worker",
1818
"start:server": "node --experimental-specifier-resolution=node ./dist/server/index.js",
1919
"start:worker": "node --experimental-specifier-resolution=node ./dist/worker/index.js",
2020
"start:docker": "docker compose build && docker compose --env-file ./.env up --remove-orphans",

server/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import { getContractExtensions } from "./contract/metadata/extensions";
33
import { readContract } from "./contract/read/read";
44
import { writeToContract } from "./contract/write/write";
55

6+
// Transactions
67
import { getAllTx } from "./transaction/getAll";
78
import { getAllDeployedContracts } from "./transaction/getAllDeployedContracts";
9+
import { retryTransaction } from "./transaction/retry";
810
import { checkTxStatus } from "./transaction/status";
911

1012
// Extensions
@@ -77,6 +79,7 @@ export const apiRoutes = async (fastify: FastifyInstance) => {
7779
await fastify.register(checkTxStatus);
7880
await fastify.register(getAllTx);
7981
await fastify.register(getAllDeployedContracts);
82+
await fastify.register(retryTransaction);
8083

8184
// Extensions
8285
await fastify.register(erc20Routes);

0 commit comments

Comments
 (0)