Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
.vscode/
build
.env
216 changes: 216 additions & 0 deletions block-finder/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# Block Finder Tool

Find the closest block after a given timestamp (in seconds) via RPC.

## Features

- βœ… Supports both second and millisecond timestamps (auto-detection and conversion)
- βœ… Uses binary search algorithm for fast and efficient searching
- βœ… Automatically estimates search range to reduce RPC calls
- βœ… Detailed progress logging with iteration information
- βœ… Supports any EVM-compatible chain
- βœ… Multi-chain support with chain-specific RPC configuration

## Installation

### 1. Install Dependencies

```bash
cd block-finder
npm install
```

### 2. Configure RPC Nodes

Create a `.env` file in the project root directory (DEX-Router-Tools-Suite):

```bash
cd ..
touch .env
```

Edit the `.env` file and add your RPC URLs for different chains:

```env
ETH_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY
BSC_RPC_URL=https://bsc-dataseed.binance.org/
ARB_RPC_URL=https://arb1.arbitrum.io/rpc
BASE_RPC_URL=https://mainnet.base.org
OP_RPC_URL=https://mainnet.optimism.io
POLYGON_RPC_URL=https://polygon-rpc.com/
```

Supported networks:
- **Ethereum**: `https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY`
- **BSC**: `https://bsc-dataseed.binance.org/`
- **Polygon**: `https://polygon-rpc.com/`
- **Arbitrum**: `https://arb1.arbitrum.io/rpc`
- **Base**: `https://mainnet.base.org`
- **Optimism**: `https://mainnet.optimism.io`

## Usage

### Basic Syntax

```bash
node findBlock.js <chain> <timestamp>
```

### Examples

```bash
cd block-finder

# Ethereum mainnet
node findBlock.js eth 1704067200

# BSC
node findBlock.js bsc 1704067200

# Arbitrum
node findBlock.js arb 1704067200

# Base
node findBlock.js base 1704067200
```

### Timestamp Format

Supports both second and millisecond timestamps:

```bash
# Second-level timestamp
node findBlock.js eth 1704067200

# Millisecond-level timestamp (auto-converted)
node findBlock.js eth 1704067200000
```

## Example Output

### Progress Logs (stderr)

```
[SEARCH] Target timestamp: 1704067200 (2024-01-01 08:00:00 UTC+8)
[SEARCH] Fetching latest block...
[SEARCH] Latest block: #18885234, timestamp: 1705305645
[SEARCH] Estimating average block time...
[SEARCH] Average block time: 12.15 seconds
[SEARCH] Search range: #18785234 ~ #18885234 (100001 blocks)
[SEARCH] Starting binary search...

[ITER 1] Block #18835234, timestamp: 1704697440, diff: +630240s
[ITER 2] Block #18810234, timestamp: 1704393840, diff: +326640s
[ITER 3] Block #18797734, timestamp: 1704241950, diff: +174750s
...
[ITER 15] Block #18783456, timestamp: 1704067201, diff: +1s

[SEARCH] Search completed in 15 iterations
[RESULT] Found block #18783456, timestamp difference: +1s
```

### Result (stdout, JSON format)

```json
{
"chain": "eth",
"blockNumber": 18783456,
"blockHash": "0x1234567890abcdef...",
"timestamp": 1704067201,
"time": "2024-01-01 08:00:01 UTC+8",
"transactions": 150
}
```

## How It Works

1. **Initialization**: Connect to RPC node and fetch latest block information
2. **Range Estimation**: Estimate starting search point based on average block time
3. **Binary Search**: Use binary search algorithm to quickly locate target block
4. **Validation**: Ensure the found block is the first one after the target timestamp

## Timestamp Utilities

To get current timestamp:

```bash
# Second-level timestamp
node -e "console.log(Math.floor(Date.now() / 1000))"

# Millisecond-level timestamp
node -e "console.log(Date.now())"

# Timestamp for a specific date
node -e "console.log(Math.floor(new Date('2024-01-01T00:00:00Z').getTime() / 1000))"
```

## API Usage

You can also use this tool as a module:

```javascript
const { findBlockByTimestamp } = require('./findBlock');
const { ethers } = require('ethers');

const provider = new ethers.providers.JsonRpcProvider('YOUR_RPC_URL');
const timestamp = 1704067200;

findBlockByTimestamp(provider, timestamp)
.then(block => {
console.log('Found block:', block.number);
});
```

## Troubleshooting

### Error: RPC_URL environment variable not found

Make sure you've created a `.env` file in the project root directory with the appropriate chain RPC configuration.

Example: For Ethereum, add `ETH_RPC_URL=https://...` to your `.env` file.

### Error: NETWORK_ERROR

Check:
- RPC URL is correct
- Network connection is stable
- API Key is valid (if using services like Alchemy, Infura, etc.)

### Target timestamp is in the future

The provided timestamp is greater than the latest block's timestamp. Please check if the timestamp is correct.

### Missing chain configuration

If you see an error like `ETH_RPC_URL environment variable not found`, add the corresponding RPC URL to your `.env` file:

```env
<CHAIN>_RPC_URL=https://your-rpc-url-here
```

Replace `<CHAIN>` with your chain name in uppercase (e.g., ETH, BSC, ARB, BASE, OP, POLYGON).

## Performance

- Uses binary search with O(log n) time complexity
- Automatically estimates search range to reduce unnecessary RPC calls
- Typically requires only 10-20 RPC requests to find the target block
- Progress logging helps track search status without impacting JSON output

## Output Format

- **Progress logs**: Output to `stderr` for tracking progress
- **Final result**: Output to `stdout` as JSON for easy parsing
- This separation allows piping the JSON output while still seeing progress:

```bash
# Save JSON to file while seeing progress
node findBlock.js eth 1704067200 > result.json

# Extract only block number
node findBlock.js eth 1704067200 2>/dev/null | jq -r '.blockNumber'
```

## License

MIT
175 changes: 175 additions & 0 deletions block-finder/findBlock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#!/usr/bin/env node
/**
* Block Finder Tool
* Find the closest block after a given timestamp (in seconds) via RPC
*/

require('dotenv').config({ path: '../.env' });
const { ethers } = require('ethers');

/**
* Binary search to find the closest block after the specified timestamp
* @param {ethers.providers.JsonRpcProvider} provider - RPC provider
* @param {number} targetTimestamp - Target timestamp (seconds)
* @returns {Promise<Object>} Block information
*/
async function findBlockByTimestamp(provider, targetTimestamp) {
const targetTime = new Date(targetTimestamp * 1000);
const utc8TargetTime = new Date(targetTime.getTime() + 8 * 60 * 60 * 1000)
.toISOString()
.replace('T', ' ')
.replace(/\.\d{3}Z$/, ' UTC+8');

console.error(`[SEARCH] Target timestamp: ${targetTimestamp} (${utc8TargetTime})`);

// Get latest block
console.error('[SEARCH] Fetching latest block...');
const latestBlock = await provider.getBlock('latest');
const latestBlockNumber = latestBlock.number;
const latestTimestamp = latestBlock.timestamp;

console.error(`[SEARCH] Latest block: #${latestBlockNumber}, timestamp: ${latestTimestamp}`);

// Check if target time is in the future
if (targetTimestamp > latestTimestamp) {
throw new Error(`Target timestamp ${targetTimestamp} is in the future, latest block timestamp is ${latestTimestamp}`);
}

// Estimate average block time
console.error('[SEARCH] Estimating average block time...');
const sampleSize = 100;
const sampleBlock = await provider.getBlock(latestBlockNumber - sampleSize);
const avgBlockTime = (latestTimestamp - sampleBlock.timestamp) / sampleSize;
console.error(`[SEARCH] Average block time: ${avgBlockTime.toFixed(2)} seconds`);

// Estimate initial search range
const timeDiff = latestTimestamp - targetTimestamp;
const estimatedBlocksBack = Math.floor(timeDiff / avgBlockTime);

let left = Math.max(0, latestBlockNumber - estimatedBlocksBack - 5000);
let right = latestBlockNumber;
let result = null;

console.error(`[SEARCH] Search range: #${left} ~ #${right} (${right - left + 1} blocks)`);
console.error('[SEARCH] Starting binary search...\n');

let iterations = 0;
// Binary search
while (left <= right) {
iterations++;
const mid = Math.floor((left + right) / 2);
const block = await provider.getBlock(mid);
const timeDiffFromTarget = block.timestamp - targetTimestamp;
const sign = timeDiffFromTarget >= 0 ? '+' : '';

console.error(`[ITER ${iterations}] Block #${mid}, timestamp: ${block.timestamp}, diff: ${sign}${timeDiffFromTarget}s`);

if (block.timestamp < targetTimestamp) {
left = mid + 1;
} else if (block.timestamp > targetTimestamp) {
right = mid - 1;
result = block;
} else {
// Exact match
console.error(`[SEARCH] Exact match found!`);
result = block;
break;
}
}

// Ensure we found the first block after the timestamp
if (result === null) {
result = await provider.getBlock(left);
}

console.error(`\n[SEARCH] Search completed in ${iterations} iterations`);
console.error(`[RESULT] Found block #${result.number}, timestamp difference: +${result.timestamp - targetTimestamp}s\n`);

return result;
}

/**
* Main function
*/
async function main() {
try {
// Get command line arguments
const args = process.argv.slice(2);

if (args.length < 2) {
console.error('Error: Please provide chain name and timestamp parameters');
console.error('Usage: node findBlock.js <chain> <timestamp>');
console.error('');
console.error('Examples:');
console.error(' node findBlock.js eth 1704067200');
console.error(' node findBlock.js bsc 1704067200');
console.error(' node findBlock.js arb 1704067200');
console.error('');
console.error('Supported chains: eth, bsc, arb, base, op, polygon, etc.');
process.exit(1);
}

const chain = args[0].toLowerCase();
let targetTimestamp = parseInt(args[1]);

// Validate timestamp
if (isNaN(targetTimestamp) || targetTimestamp <= 0) {
console.error('Error: Invalid timestamp');
process.exit(1);
}

// Automatically handle millisecond timestamps
if (targetTimestamp > 10000000000) {
targetTimestamp = Math.floor(targetTimestamp / 1000);
}

// Get RPC URL for the specified chain
const envVarName = `${chain.toUpperCase()}_RPC_URL`;
const rpcUrl = process.env[envVarName];

if (!rpcUrl) {
console.error(`Error: ${envVarName} environment variable not found`);
console.error(`Please add ${envVarName} to your .env file in project root`);
console.error('');
console.error('Example .env file:');
console.error(' ETH_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY');
console.error(' BSC_RPC_URL=https://bsc-dataseed.binance.org/');
console.error(' ARB_RPC_URL=https://arb1.arbitrum.io/rpc');
process.exit(1);
}

// Create provider
const provider = new ethers.providers.JsonRpcProvider(rpcUrl);

// Find block
const block = await findBlockByTimestamp(provider, targetTimestamp);

// Output result (UTC+8 time)
const date = new Date(block.timestamp * 1000);
const utc8Time = new Date(date.getTime() + 8 * 60 * 60 * 1000)
.toISOString()
.replace('T', ' ')
.replace(/\.\d{3}Z$/, ' UTC+8');

console.log(JSON.stringify({
chain: chain,
blockNumber: block.number,
blockHash: block.hash,
timestamp: block.timestamp,
time: utc8Time,
transactions: block.transactions.length
}, null, 2));

} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}
}

// Run main function
if (require.main === module) {
main();
}

module.exports = { findBlockByTimestamp };

Loading