Skip to content

Commit 85cccc6

Browse files
committedAug 19, 2024
📝 Documentation.
1 parent 6221d53 commit 85cccc6

15 files changed

+156
-27
lines changed
 

‎README.md

+115-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,119 @@
11
# mempool-listener
22
A mempool listener for contract specific transactions.
33

4+
## ⚠️ Warning
5+
This implementation is for educational purposes and not for production use. The tests carried out with this listener were all done on testnet networks and uses specific RPC endpoints that support the `eth_newPendingTransaction` API. It is nice to note that not all RPC or WSS endpoints support this API.
6+
7+
## Links
8+
[GitHub Repository](https://github.com/0xfps/mempool-listener)<br>
9+
[Node Package Manager (NPM)](https://www.npmjs.com/package/mempool-listener)
10+
411
## Quick Explanation
5-
Assuming we want to set up such a mempool listener for transactions that call the [`mint()`](https://sepolia.arbiscan.io/writecontract/index?m=light&v=21.10.1.1&a=0xe3Bd885d1e0e39Db79f0375AE41048057199F344&n=arbsepolia&p=#collapse2) function at [this contract address](https://sepolia.arbiscan.io/address/0xe3Bd885d1e0e39Db79f0375AE41048057199F344) on Arbitrum Sepolia, this listener will only pick up and return transactions that the data key of their transaction object starts with the function signature `0x40c10f19`. This is achieved by setting up a provider with the Arbitrum Sepolia WSS RPC, configured by the user, not by the code, and using the provider event listeners and then filtering out and returning only the [`mint()`](https://sepolia.arbiscan.io/writecontract/index?m=light&v=21.10.1.1&a=0xe3Bd885d1e0e39Db79f0375AE41048057199F344&n=arbsepolia&p=#collapse2) transactions sent to that contract address that are in the mempool. We can use the ABI for the configured transaction to be listened for to try to extract the values sent as arguments to the transaction and we can try to do stuff with that. Refer to [this article](https://www.showwcase.com/article/14647/how-to-listen-to-pending-transactions-using-ethersjs) on how this can be achieved.
12+
Assuming we want to set up such a mempool listener for transactions that were sent due to a call on the [`handleOps()`](https://sepolia.etherscan.io/writecontract/index?m=light&v=21.10.1.1&a=0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789&n=sepolia&p=#collapse5) function at [this contract address](https://sepolia.etherscan.io/writecontract/index?m=light&v=21.10.1.1&a=0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789) on Ethereum Sepolia, this listener will only pick up transactions that the data key of their transaction object starts with the function signature `0x1fad948c` and call a set `executableFunction` the listener has been configured with. This is achieved by setting up a provider with a user chosen Ethereum Sepolia WSS or RPC endpoint, and using the provider's event listeners, filter out and work with only the [`handleOps()`](https://sepolia.etherscan.io/writecontract/index?m=light&v=21.10.1.1&a=0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789&n=sepolia&p=#collapse5) transactions sent to that contract address that are in the mempool. We can use the ABI for the configured transaction to be listened for to try to extract the values sent as arguments to the transaction and we can try to do stuff with that in the `executableFunction` as the user desires. Refer to [this article](https://www.showwcase.com/article/14647/how-to-listen-to-pending-transactions-using-ethersjs) on how this can be achieved.
13+
14+
## Collaborating
15+
If you happen to use this package, and run into some bug, or have some ideas on how I can improve the functionalities, please reach out by opening an [issue](https://github.com/0xfps/mempool-listener/issues). You can also fix it yourself and make a pull request. Thanks, and I appreciate your use of this package.
16+
17+
## How To Use
18+
Import package from NPM.
19+
```shell
20+
npm install mempool-listener
21+
```
22+
23+
Import `MempoolListener` into your code file and initialize it with your chosen RPC or WSS endpoint URL. Your RPC or WSS URL should support the [`eth_newPendingTransaction`](https://etclabscore.github.io/core-geth/JSON-RPC-API/modules/eth/#eth_newpendingtransactions) API.
24+
```ts
25+
// TypeScript.
26+
import MempoolListener from "mempool-listener"
27+
const mempoolListener = new MempoolListener("RPC or WSS URL")
28+
```
29+
30+
```js
31+
// JavaScript.
32+
const MempoolListener = require("mempool-listener").default
33+
const mempoolListener = new MempoolListener("RPC or WSS URL")
34+
```
35+
36+
OR
37+
38+
```js
39+
// JavaScript.
40+
const { default: MempoolListener } = require("mempool-listener")
41+
const mempoolListener = new MempoolListener("RPC or WSS URL")
42+
```
43+
44+
Configure the ABI of the contract, the contract address and function name to listen to. Also, configure your executable function, in this case called `executableFunc`. This is the function that gets called whenever a transaction that matches the `functionName` is picked up by the listener. `executableFunc` **MUST** have one parameter, `args` that is an object containing the arguments in the picked up pending transaction, the value sent to the call, and the gas price paid for the transaction.
45+
46+
```ts
47+
// TypeScript.
48+
import { Abi } from "mempool-listener/build/types/abi-types"
49+
import { ListenerConfig } from "mempool-listener/build/types/listener-config-types"
50+
51+
const config = {
52+
address: "0xabcdef",
53+
abi: ["Contract Abi"] as Abi,
54+
functionName: "functionName"
55+
}
56+
57+
function executableFunc(args) {
58+
console.log(args)
59+
}
60+
```
61+
62+
```js
63+
// JavaScript.
64+
import { Abi } from "mempool-listener/build/types/abi-types"
65+
import { ListenerConfig } from "mempool-listener/build/types/listener-config-types"
66+
67+
const config = {
68+
address: "0xabcdef",
69+
abi: ["Contract Abi"] as Abi,
70+
functionName: "functionName"
71+
}
72+
73+
function executableFunc(args) {
74+
console.log(args)
75+
}
76+
```
77+
78+
Finally, you can start up your listener passing the `config` and `executableFunc` as arguments.
79+
80+
```ts
81+
// TypeScript.
82+
import MempoolListener from "mempool-listener"
83+
const mempoolListener = new MempoolListener("RPC or WSS URL")
84+
mempoolListener.listen(config, executableFunc)
85+
```
86+
87+
```js
88+
// JavaScript.
89+
const MempoolListener = require("mempool-listener").default
90+
const mempoolListener = new MempoolListener("RPC or WSS URL")
91+
mempoolListener.listen(config, executableFunc)
92+
```
93+
94+
Whenever a pending transaction is picked up, it checks for the first four bytes of the `data` key in the transaction data and tries to match it with the selector of the function name you passed in your config. If these two selectors match, the `executableFunc` is called.
95+
96+
You can stop the listener temporarily by calling the `stopListener` function, and, you can restart the listener by calling the `restartListener` function.
97+
98+
```ts
99+
// TypeScript.
100+
import MempoolListener from "mempool-listener"
101+
const mempoolListener = new MempoolListener("RPC or WSS URL")
102+
mempoolListener.listen(config, executableFunc)
103+
mempoolListener.stopListener()
104+
mempoolListener.restartListener()
105+
```
106+
107+
```js
108+
// JavaScript.
109+
const MempoolListener = require("mempool-listener").default
110+
const mempoolListener = new MempoolListener("RPC or WSS URL")
111+
mempoolListener.listen(config, executableFunc)
112+
mempoolListener.stopListener()
113+
mempoolListener.restartListener()
114+
```
115+
116+
Trying to stop or restart an undefined listener will do nothing.
117+
118+
## License
119+
GPL-3.0.

‎build/index.d.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ declare class MempoolListener {
2323
* runs whenever a pending transaction made to `functionName` is picked up.
2424
*
2525
* The `executableFunction` requires one parameter, `args`, an object containing
26-
* the arguments in the picked up pending transaction and the value sent to the call.
26+
* the arguments in the picked up pending transaction, the value sent to the call,
27+
* and the gas price paid for the transaction.
2728
*
28-
* @param args An object of arguments from the picked up transaction, (`args`)
29-
* and the value sent along the contract call, (`value`).
29+
* @param args An object of arguments from the picked up transaction, (`args`),
30+
* the value sent along the contract call, (`value`) and the gas price
31+
* paid for the transaction, (`gasPrice`).
3032
*/
3133
executableFunction: (args: any) => any;
3234
/**
@@ -52,6 +54,12 @@ declare class MempoolListener {
5254
* neither does it remove any class state.
5355
*/
5456
stopListener(): void;
57+
/**
58+
* Restarts the listening process. Since the `address`, `abi` and `functionName`
59+
* are stored in the class already, it simply only turns on the listener again,
60+
* preventing repassing the config and executable function.
61+
*/
62+
restartListener(): void;
5563
/**
5664
* This function is called whenever a transaction is picked up by the listener. Then,
5765
* using the hash returned by the listener, returns the parent transaction and then

‎build/index.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ class MempoolListener {
6262
if (this.PROVIDER)
6363
this.PROVIDER.off("pending", this.handlePendingTransaction);
6464
}
65+
/**
66+
* Restarts the listening process. Since the `address`, `abi` and `functionName`
67+
* are stored in the class already, it simply only turns on the listener again,
68+
* preventing repassing the config and executable function.
69+
*/
70+
restartListener() {
71+
if (this.PROVIDER)
72+
this.PROVIDER.on("pending", this.handlePendingTransaction);
73+
}
6574
/**
6675
* This function is called whenever a transaction is picked up by the listener. Then,
6776
* using the hash returned by the listener, returns the parent transaction and then
@@ -83,14 +92,14 @@ class MempoolListener {
8392
return __awaiter(this, void 0, void 0, function* () {
8493
const tx = yield this.PROVIDER.getTransaction(txHash);
8594
if (tx) {
86-
const { data, value, to } = tx;
95+
const { data, value, to, gasPrice } = tx;
8796
const transactionFunctionSignature = data.slice(0, 10);
8897
const selector = this.selector;
8998
if (selector && (transactionFunctionSignature == selector) && (to == this.address)) {
9099
const decodedData = (0, decode_transaction_data_1.decodeTransactionData)(this.ABI, { data, value });
91100
if (decodedData) {
92101
const { args: txArgs } = decodedData;
93-
const args = { args: txArgs, value };
102+
const args = { args: txArgs, value, gasPrice };
94103
this.executableFunction(args);
95104
}
96105
}

‎build/types/abi-types.js

-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,2 @@
11
"use strict";
2-
/**
3-
* ABI Types.
4-
*
5-
* This is a representation of the components of a Solidity ABI
6-
* in their respective types.
7-
*/
82
Object.defineProperty(exports, "__esModule", { value: true });

‎package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
{
22
"name": "mempool-listener",
3-
"version": "0.0.0",
3+
"version": "0.0.1",
44
"description": "A mempool listener for contract specific transactions.",
55
"main": "build/index.js",
6+
"types": "build/index.d.ts",
67
"scripts": {
78
"clean": "rm -rf build && tsc",
89
"start": "tsc && node build/index.js",

‎src/index.ts

+17-5
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@ class MempoolListener {
3131
* runs whenever a pending transaction made to `functionName` is picked up.
3232
*
3333
* The `executableFunction` requires one parameter, `args`, an object containing
34-
* the arguments in the picked up pending transaction and the value sent to the call.
34+
* the arguments in the picked up pending transaction, the value sent to the call,
35+
* and the gas price paid for the transaction.
3536
*
36-
* @param args An object of arguments from the picked up transaction, (`args`)
37-
* and the value sent along the contract call, (`value`).
37+
* @param args An object of arguments from the picked up transaction, (`args`),
38+
* the value sent along the contract call, (`value`) and the gas price
39+
* paid for the transaction, (`gasPrice`).
3840
*/
3941
public executableFunction!: (args: any) => any
4042

@@ -89,6 +91,16 @@ class MempoolListener {
8991
this.PROVIDER.off("pending", this.handlePendingTransaction)
9092
}
9193

94+
/**
95+
* Restarts the listening process. Since the `address`, `abi` and `functionName`
96+
* are stored in the class already, it simply only turns on the listener again,
97+
* preventing repassing the config and executable function.
98+
*/
99+
restartListener() {
100+
if (this.PROVIDER)
101+
this.PROVIDER.on("pending", this.handlePendingTransaction)
102+
}
103+
92104
/**
93105
* This function is called whenever a transaction is picked up by the listener. Then,
94106
* using the hash returned by the listener, returns the parent transaction and then
@@ -110,15 +122,15 @@ class MempoolListener {
110122
const tx: TransactionType = await this.PROVIDER.getTransaction(txHash) as unknown as TransactionType
111123

112124
if (tx) {
113-
const { data, value, to } = tx
125+
const { data, value, to, gasPrice } = tx
114126
const transactionFunctionSignature = data.slice(0, 10)
115127
const selector = this.selector
116128

117129
if (selector && (transactionFunctionSignature == selector) && (to == this.address)) {
118130
const decodedData = decodeTransactionData(this.ABI, { data, value } as TransactionType)
119131
if (decodedData) {
120132
const { args: txArgs } = decodedData
121-
const args = { args: txArgs, value }
133+
const args = { args: txArgs, value, gasPrice }
122134
this.executableFunction(args)
123135
}
124136
}

‎src/tests/abis/entry-point-abi.ts

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import EntryPoint from "./json/EntryPoint.json"
66
*
77
* https://sepolia.etherscan.io/address/0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789
88
*/
9-
109
export const ENTRY_POINT = {
1110
abi: EntryPoint
1211
}

‎src/tests/constants.ts

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { ENTRY_POINT } from "./abis/entry-point-abi";
44
* Configurations for different chains for testing.
55
* `functionName` is left out for the purposes of flexibility.
66
*/
7-
87
export const ChainListenerConfigs = {
98
sepolia: {
109
abis: [ENTRY_POINT.abi],

‎src/types/abi-types.ts

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
* This is a representation of the components of a Solidity ABI
55
* in their respective types.
66
*/
7-
87
type AbiInput = {
98
indexed?: boolean
109
components?: AbiInput[],

‎src/types/listener-config-types.ts

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { Abi } from "./abi-types"
77
* @param abi ABI of contract at `address`.
88
* @param functionName Name of function to listen for.
99
*/
10-
1110
export type ListenerConfig = {
1211
address: string,
1312
abi: Abi,

‎src/types/transaction-data-types.ts

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { BigNumberish } from "ethers"
33
/**
44
* Returned and parsed transaction data.
55
*/
6-
76
export type TransactionType = {
87
hash: string,
98
from: string,

‎src/utils/check-address.ts

-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { ethers } from "ethers";
55
*
66
* @param address Address to validate.
77
*/
8-
98
export function checkAddress(address: string) {
109
if (ethers.isAddress(address)) return
1110
throw new Error(`${address} is not a valid address.`)

‎src/utils/decode-transaction-data.ts

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { ParsedTransaction, TransactionType } from "../types/transaction-data-ty
1212
*
1313
* @returns ParsedTransaction | null Parsed transaction data.
1414
*/
15-
1615
export function decodeTransactionData(abi: Abi, { data, value }: TransactionType): ParsedTransaction | null {
1716
const descr = new ethers.Interface(abi)
1817
const parsedTransaction = descr.parseTransaction({ data, value }) as unknown as ParsedTransaction

‎src/utils/encode-function-with-signature.ts

-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { Abi } from "../types/abi-types";
99
*
1010
* @returns string Function selector.
1111
*/
12-
1312
export function encodeFunctionWithSignature(abi: Abi, functionName: string): string {
1413
const selectedAbi = abi.find(function (abi) {
1514
return ((abi.type == "function") && (abi.name == functionName))

‎src/utils/return-functions-from-abi.ts

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { Abi } from "../types/abi-types";
88
*
99
* @returns string[] Array of function names.
1010
*/
11-
1211
export function returnFunctionsFromAbi(abi: Abi): string[] {
1312
return abi.filter(function (abi) {
1413
return abi.type == "function"

0 commit comments

Comments
 (0)