/
track-tx.ts
129 lines (115 loc) · 3.6 KB
/
track-tx.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import { Command, flags } from "@oclif/command";
import { createInfuraProvider } from "../providers";
import { parseNetwork } from "../networks";
import { Semaphore } from "await-semaphore";
import { providers } from "ethers";
import { BlockWithTransactions } from "@ethersproject/abstract-provider";
import { isDefined } from "ts-is-present";
export default class TrackTransactions extends Command {
static description =
"Find transactions involving an address in a slice of blocks";
static examples = [
"$ eth-client track-tx --start -5 --concurrency 5",
"$ eth-client track-tx 0xdAC17F958D2ee523a2206206994597C13D831ec7 --start 5 --end 10",
];
static flags = {
help: flags.help({
char: "h",
}),
network: flags.enum({
options: ["homestead"],
default: "homestead",
description: "The network to search on",
}),
start: flags.integer({
required: true,
description:
"Index representing the inclusive start of the block range to query",
}),
end: flags.integer({
description:
"Index representing the exclusive end of the block range to query",
}),
concurrency: flags.integer({
default: 10,
description: "Maximum number of concurrent requests for data fetching",
}),
pretty: flags.boolean({
description: "Pretty print the json output instead of using ndjson",
}),
};
static args = [
{
name: "address",
required: false,
description: "The address to inspect transactions for",
},
];
async run() {
const { args, flags } = this.parse(TrackTransactions);
const provider = createInfuraProvider(parseNetwork(flags.network)!);
const lastBlock = await provider.getBlockNumber();
const start = flags.start < 0 ? lastBlock + flags.start + 1 : flags.start;
const end = flags.end ?? lastBlock + 1;
const blocks = await blockFetch({
provider,
concurrency: flags.concurrency,
start,
end,
});
const relevantBlocks = blocks
.map((b) => extractRelevantTransactions(b, args.address))
.filter(isDefined);
if (flags.pretty) {
this.log(JSON.stringify(relevantBlocks, null, 2));
} else {
// Default formatting to ndjson
relevantBlocks.forEach((b) => this.log(JSON.stringify(b)));
}
}
}
interface BlockFetchParameters {
readonly provider: providers.Provider;
readonly concurrency: number;
readonly start: number;
readonly end: number;
}
function blockFetch(
params: BlockFetchParameters
): Promise<BlockWithTransactions[]> {
if (params.end <= params.start) {
throw new Error(`Illegal block range: ${params.start}..${params.end}`);
}
const semaphore = new Semaphore(params.concurrency);
const blockPromises = [];
for (let i = params.start; i < params.end; i++) {
blockPromises.push(
semaphore.use(() => params.provider.getBlockWithTransactions(i))
);
}
return Promise.all(blockPromises);
}
function extractRelevantTransactions(
block: BlockWithTransactions,
target: string | undefined
): BlockWithTransactions | undefined {
if (target === undefined) {
return block;
} else if (!block.transactions.some((tx) => tx.to === target)) {
return undefined;
} else {
return {
hash: block.hash,
difficulty: block.difficulty,
extraData: block.extraData,
gasLimit: block.gasLimit,
gasUsed: block.gasUsed,
miner: block.miner,
nonce: block.nonce,
number: block.number,
parentHash: block.parentHash,
timestamp: block.timestamp,
transactions: block.transactions.filter((tx) => tx.to === target),
};
}
}