Your codebase has 200+ lines of duplicate code and 6 major patterns that repeat across files. This guide shows exactly how to refactor starting with the highest-impact item.
// In web3_instance.ts, transfer_for_gas.ts, bitcoin_raw_signer.ts, etc.
while (
currentStatus !== TransactionStatus.COMPLETED &&
currentStatus !== TransactionStatus.FAILED &&
currentStatus !== TransactionStatus.BLOCKED &&
currentStatus !== TransactionStatus.CANCELLED
) {
try {
console.log(`Polling for tx ${txid}; status: ${currentStatus}`);
txInfo = await fireblocksApiClient.getTransactionById(txid);
currentStatus = txInfo.status;
} catch (err) {
console.error("Error while polling transaction:", err);
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
if (currentStatus === TransactionStatus.FAILED ||
currentStatus === TransactionStatus.BLOCKED ||
currentStatus === TransactionStatus.REJECTED) {
await fireblocksApiClient.cancelTransactionById(txid);
}File: src/utils/TransactionPoller.ts
import {
FireblocksSDK,
TransactionStatus,
FireblocksTransaction,
} from "fireblocks-sdk";
export interface PollingOptions {
maxAttempts?: number;
intervalMs?: number;
autoCancel?: boolean;
}
export class TransactionPoller {
private static readonly DEFAULT_INTERVAL_MS = 1000;
private static readonly TERMINAL_STATUSES = [
TransactionStatus.COMPLETED,
TransactionStatus.FAILED,
TransactionStatus.BLOCKED,
TransactionStatus.CANCELLED,
];
private static readonly CANCELABLE_STATUSES = [
TransactionStatus.FAILED,
TransactionStatus.BLOCKED,
TransactionStatus.REJECTED,
];
constructor(
private fireblocksApi: FireblocksSDK,
private readonly options: PollingOptions = {}
) {}
async poll(
transactionId: string,
onStatusChange?: (status: string) => void
): Promise<FireblocksTransaction> {
const maxAttempts = this.options.maxAttempts ?? Infinity;
const intervalMs = this.options.intervalMs ?? TransactionPoller.DEFAULT_INTERVAL_MS;
const autoCancel = this.options.autoCancel !== false;
let currentStatus: string | undefined;
let txInfo: FireblocksTransaction | undefined;
let attempts = 0;
while (
attempts < maxAttempts &&
!TransactionPoller.isTerminalStatus(currentStatus)
) {
try {
txInfo = await this.fireblocksApi.getTransactionById(transactionId);
currentStatus = txInfo.status;
if (onStatusChange) {
onStatusChange(currentStatus);
}
} catch (error) {
console.error(`Error polling transaction ${transactionId}:`, error);
throw error;
}
if (!TransactionPoller.isTerminalStatus(currentStatus)) {
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
attempts++;
}
if (!txInfo) {
throw new Error(`Failed to retrieve transaction ${transactionId}`);
}
// Auto-cancel failed transactions if enabled
if (autoCancel && TransactionPoller.isCancelableStatus(currentStatus)) {
await this.cancelTransaction(transactionId);
}
return txInfo;
}
private async cancelTransaction(transactionId: string): Promise<void> {
try {
await this.fireblocksApi.cancelTransactionById(transactionId);
console.log(`Cancelled transaction ${transactionId}`);
} catch (error) {
console.error(`Failed to cancel transaction ${transactionId}:`, error);
}
}
private static isTerminalStatus(status?: string): boolean {
return status ? this.TERMINAL_STATUSES.includes(status as TransactionStatus) : false;
}
private static isCancelableStatus(status?: string): boolean {
return status ? this.CANCELABLE_STATUSES.includes(status as TransactionStatus) : false;
}
}Before (consolidate_unsupported_assets_with_raw_transactions.ts):
while (currentStatus !== TransactionStatus.COMPLETED && ...) {
try {
const txInfo = await fireblocksApiClient.getTransactionById(txid);
currentStatus = txInfo.status;
} catch (err) {
console.error("Error while polling transaction:", err);
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
if (currentStatus === TransactionStatus.FAILED || ...) {
await fireblocksApiClient.cancelTransactionById(txid);
}After:
import { TransactionPoller } from "../utils/TransactionPoller";
const poller = new TransactionPoller(fireblocksApiClient, {
maxAttempts: 60,
intervalMs: 1000,
autoCancel: true,
});
const txInfo = await poller.poll(txid, (status) => {
console.log(`Transaction status: ${status}`);
});EVM/web3_instance.ts- Replace lines 87-117EVM/transfer_for_gas.ts- Replace lines 24-60EVM/deposit_eth_gas_to_vault.ts- Replace lines 22-61EVM/deposit_gas_to_many_specified_tokens_and_chains.ts- Replace lines 59-94Bitcoin/bitcoin_raw_signer.ts- Replace lines 75-100
Impact: Removes 100+ lines of duplicate code, makes polling consistent across codebase
// Gas and Transaction Configuration
export const GAS_CONFIG = {
NATIVE_TRANSFER_LIMIT: 21000,
ESTIMATION_BUFFER: 1.2,
POLLING_INTERVAL_MS: 1000,
};
// Balance Thresholds
export const BALANCE_THRESHOLDS = {
MIN_GAS_BALANCE_ETH: 0.0005,
MIN_BALANCE_CHECK_ETH: 0.0001,
MIN_BALANCE_SEARCH_ETH: 0.0009,
};
// RPC Endpoints
export const RPC_ENDPOINTS = {
ETHEREUM_FLASHBOTS: 'https://rpc.flashbots.net',
BSC: 'https://bsc-dataseed1.bnbchain.org',
};
// Color Codes (ANSI)
export const COLOR_CODES = {
RED: '31',
GREEN: '32',
YELLOW: '33',
MAGENTA: '35',
CYAN: '36',
};
// Asset Configurations
export const ASSET_IDS = {
ETH: 'ETH',
ETH_TEST3: 'ETH_TEST3',
MATIC: 'MATIC',
MATIC_POLYGON: 'MATIC_POLYGON',
} as const;
// Peer Types
export const PEER_TYPES = {
VAULT_ACCOUNT: 'VAULT_ACCOUNT',
ONE_TIME_ADDRESS: 'ONE_TIME_ADDRESS',
} as const;Before:
const gasLimit = Math.floor(estimatedGas * 1.2); // What is 1.2?
const minimumGasBalance = 0.0005; // Magic number
const httpProviderURL = 'https://rpc.flashbots.net'; // Hardcoded
console.log(colorLog("message", "32")); // What is "32"?After:
import { GAS_CONFIG, BALANCE_THRESHOLDS, RPC_ENDPOINTS, COLOR_CODES } from "../constants";
const gasLimit = Math.floor(estimatedGas * GAS_CONFIG.ESTIMATION_BUFFER);
const minimumGasBalance = BALANCE_THRESHOLDS.MIN_GAS_BALANCE_ETH;
const httpProviderURL = RPC_ENDPOINTS.ETHEREUM_FLASHBOTS;
console.log(colorLog("message", COLOR_CODES.GREEN));import { FireblocksSDK } from "fireblocks-sdk";
import Web3 from "web3";
// Fireblocks-related types
export interface FireblocksConfig {
apiSecret: string;
apiKey: string;
}
export interface DepositAddress {
address: string;
tag?: string;
type?: string;
}
// Transaction-related types
export interface TransactionOptions {
existingTransactionId?: string;
autoCancel?: boolean;
maxAttempts?: number;
}
export interface TransferRequest {
fireblocksApiClient: FireblocksSDK;
ethereumProviderUrl: string;
sourceVaultAccountId: string | number;
recipientAddress: string;
assetIdentifier: string;
assetSymbol: string;
transferAmount: number;
erc20ContractAddress?: string;
transactionFilename?: string;
existingTransactionId?: string;
destinationVault?: number;
}
// Balance-related types
export interface BalanceCheckResult {
address: string;
balance: string;
}
export interface TokenBalance {
tokenBalance: string;
nativeBalance: string;
}
// CSV-related types
export interface CryptoData {
Coin: string;
Network: string;
Vaults: string;
RowNumber: number;
Balance: string;
}
export interface ContractData {
Coin: string;
"Token Name": string;
Contract: string;
}
export interface ChainData {
Network: string;
RPC: string;
}
export interface VaultData {
Vault: string;
NativeToken: string;
Address: string;
Coin: string;
}Before:
export async function transfer(
fireblocksApiClient,
ethereumProviderUrl,
sourceVaultAccountId,
recipientAddress,
assetIdentifier,
assetSymbol,
transferAmount = 0,
erc20ContractAddress?,
transactionFilename?,
existingTransactionId?,
destinationVault = 0
) {
// ...
}After:
import { TransferRequest } from "../types";
export async function transfer(request: TransferRequest): Promise<void> {
const {
fireblocksApiClient,
ethereumProviderUrl,
sourceVaultAccountId,
recipientAddress,
assetIdentifier,
assetSymbol,
transferAmount = 0,
erc20ContractAddress,
transactionFilename,
existingTransactionId,
destinationVault = 0,
} = request;
// Implementation
}- Day 1: Fix undefined variable bug in
bitcoin_raw_signer.ts:90 - Day 2: Create
TransactionPollerutility class - Day 3: Create
constants.tsfile - Day 4: Create
types.tsfile - Day 5: Update 6 files to use
TransactionPoller
- Day 6-7: Remove duplicate Balance Checker class
- Day 8: Extract CLI Argument Parser
- Day 9-10: Add return types to public functions
- Extract ERC20 Transfer Handler
- Refactor
initWeb3Instance() - Create Error Handling utilities
Before and after each refactoring:
- Code compiles without errors
- No new TypeScript warnings
- Existing tests still pass (if any)
- Manual testing on test network
- Verify transaction polling works as expected
- Check colored console output displays correctly
- Confirm error handling catches exceptions properly
If something breaks:
- Commit working version before refactoring
- Keep old code in separate branch
- Test each change independently
- Use git bisect to find problematic commit
# Create backup branch
git checkout -b refactoring-backup
# Make changes on main branch
git checkout main
# If issue found, compare with backup
git diff refactoring-backup- Reduce duplicate code by 75% (200 → 50 lines)
- Reduce magic numbers from 40+ to 5
- Increase type coverage from 30% to 95%
- Reduce average function length from 35 to 25 lines
- Improve error handling coverage from 40% to 75%
- Changes to polling logic only needed in 1 place instead of 6
- New developers can understand patterns more easily
- IDE can provide better autocomplete with proper types
- Tests become easier to write with clear interfaces
See REFACTORING_ANALYSIS.md for complete details on all findings.