@flashbots/matchmaker-ts
Advanced tools
Comparing version 0.4.3 to 0.5.0
{ | ||
"name": "@flashbots/matchmaker-ts", | ||
"version": "0.4.3", | ||
"version": "0.5.0", | ||
"description": "Matchmaker client library for Flashbots mev-share.", | ||
@@ -12,4 +12,4 @@ "main": "build/index.js", | ||
"axios": "^1.3.4", | ||
"eventsource": "^2.0.2", | ||
"ethers": "^6.1.0" | ||
"ethers": "^6.3.0", | ||
"eventsource": "^2.0.2" | ||
}, | ||
@@ -23,3 +23,2 @@ "devDependencies": { | ||
"eslint": "^8.36.0", | ||
"ethers": "^6.1.0", | ||
"release-it": "^15.8.0", | ||
@@ -30,3 +29,3 @@ "ts-node": "^10.9.1", | ||
"peerDependencies": { | ||
"ethers": "^6.1.0" | ||
"ethers": "^6.3.0" | ||
}, | ||
@@ -33,0 +32,0 @@ "scripts": { |
121
README.md
@@ -33,9 +33,60 @@ # Flashbots Matchmaker | ||
import { Wallet, JsonRpcProvider } from "ethers" | ||
import Matchmaker, { ShareBundleParams, PendingShareTransaction, ShareTransactionOptions } from "@flashbots/matchmaker-ts" | ||
import Matchmaker, { | ||
BundleParams, | ||
HintPreferences, | ||
IPendingBundle, | ||
IPendingTransaction, | ||
StreamEvent, | ||
TransactionOptions | ||
} from "@flashbots/matchmaker-ts" | ||
const provider = new JsonRpcProvider(GOERLI_RPC_URL) | ||
const provider = new JsonRpcProvider(RPC_URL) | ||
const authSigner = new Wallet(FB_REPUTATION_PRIVATE_KEY, provider) | ||
const matchmaker = new Matchmaker(authSigner, {chainId: 5, name: "goerli"}) | ||
``` | ||
The `Matchmaker` class has built-in initializers for networks supported by Flashbots. | ||
#### Connect to Ethereum Mainnet | ||
```typescript | ||
const matchmaker = Matchmaker.useEthereumMainnet(authSigner) | ||
``` | ||
#### Connect to Ethereum Goerli | ||
```typescript | ||
const matchmaker = Matchmaker.useEthereumGoerli(authSigner) | ||
``` | ||
#### Connect with an Ethers Provider or Chain ID | ||
Networks supported by Flashbots have presets built-in. If it's more convenient, you can instantiate a Matchmaker using a `chainId` (or a ethers.js `Network` object, which has a `chainId` param). | ||
```typescript | ||
import { JsonRpcProvider } from "ethers" // ethers v6 | ||
/** connects to Flashbots matchmaker on goerli */ | ||
function connectMatchmaker(provider: JsonRpcProvider) { | ||
return Matchmaker.fromNetwork(provider._network) | ||
} | ||
// manually with a chainId: | ||
const matchmaker = Matchmaker.fromNetwork({chainId: 5}) | ||
``` | ||
#### Connect to a custom network | ||
To use custom network parameters, you can instantiate a new Matchmaker directly. This example is what the client uses to connect to mainnet: | ||
```typescript | ||
const matchmaker = new Matchmaker(authSigner, { | ||
name: "mainnet", | ||
chainId: 1, | ||
streamUrl: "https://mev-share.flashbots.net", | ||
apiUrl: "https://relay.flashbots.net" | ||
}) | ||
``` | ||
See [MatchmakerNetwork](/src/api/interfaces.ts#L15) for more details. | ||
### examples | ||
@@ -55,3 +106,3 @@ | ||
This example sends a transaction to mev-share from the account specified by SENDER_PRIVATE_KEY with a hex-encoded string as data. | ||
This example sends a transaction to the Flashbots Goerli Matchmaker from the account specified by SENDER_PRIVATE_KEY with a hex-encoded string as calldata. | ||
@@ -64,3 +115,3 @@ ```sh | ||
This example watches the mev-share streaming endpoint for pending mev-share transactions and attempts to backrun them. The example runs until a backrun has been included on-chain. | ||
This example watches the mev-share streaming endpoint for pending mev-share transactions and attempts to backrun them all. The example runs until a backrun has been included on-chain. | ||
@@ -71,21 +122,25 @@ ```sh | ||
## API | ||
## Usage | ||
### `onShareTransaction` | ||
See [src/api/interfaces.ts](src/api/interfaces.ts) for interface definitions. | ||
Starts listening for transactions on mev-share, registers the provided callback to be called when a new transaction is detected. | ||
### `on` | ||
Use `on` to start listening for events on mev-share. The function registers the provided callback to be called when a new event is detected. | ||
```typescript | ||
const callback = (tx: PendingShareTransaction) => {/* handle pending tx */} | ||
const handler = matchmaker.onShareTransaction(callback) | ||
// do some stuff... | ||
const handler = matchmaker.on(StreamEvent.Transaction, (tx: IPendingTransaction) => { | ||
// handle pending tx | ||
}) | ||
// before terminating program | ||
handler.close() | ||
``` | ||
### `sendShareTransaction` | ||
### `sendTransaction` | ||
Sends a private transaction to Flashbots with specified hint parameters. | ||
Sends a private transaction to the Flashbots Matchmaker with specified hint parameters. | ||
```typescript | ||
const shareTxParams: ShareTransactionOptions = { | ||
const shareTxParams: TransactionOptions = { | ||
hints: { | ||
@@ -95,20 +150,40 @@ logs: true, | ||
functionSelector: true, | ||
contractAddress: true | ||
contractAddress: true, | ||
}, | ||
maxBlockNumber: undefined, | ||
} | ||
await matchmaker.sendShareTransaction(SIGNED_TX, shareTxParams) | ||
await matchmaker.sendTransaction(SIGNED_TX, shareTxParams) | ||
``` | ||
### `sendShareBundle` | ||
### `sendBundle` | ||
Sends a bundle; an array of transactions; which tries to backrun a pending mev-share transaction. Currently only one share transaction in `shareTxs` is supported. | ||
Sends a bundle; an array of transactions with parameters to specify conditions for inclusion and MEV kickbacks. Transactions are placed in the `body` parameter with wrappers to indicate whether they're a new signed transaction or a pending transaction from the event stream. | ||
See [MEV-Share Docs](https://github.com/flashbots/mev-share/blob/main/src/mev_sendBundle.md) for detailed descriptions of these parameters. | ||
```typescript | ||
const bundleParams: ShareBundleParams = { | ||
targetBlock: TARGET_BLOCK, | ||
shareTxs: [PENDING_TX_HASH], | ||
backrun: [SIGNED_BACKRUN_TX1, SIGNED_BACKRUN_TX2], | ||
const bundleParams: BundleParams = { | ||
inclusion: { | ||
block: TARGET_BLOCK, | ||
}, | ||
body: [ | ||
{hash: TX_HASH_FROM_EVENT_STREAM}, | ||
{tx: SIGNED_TX, canRevert: false}, | ||
], | ||
validity: { | ||
refund: [ | ||
{address: SEARCHER_ADDRESS, percent: 10} | ||
] | ||
}, | ||
privacy: { | ||
hints: { | ||
calldata: false, | ||
logs: false, | ||
functionSelector: true, | ||
contractAddress: true, | ||
}, | ||
targetBuilders: ["all"] | ||
} | ||
} | ||
await matchmaker.sendShareBundle(bundleParams) | ||
await matchmaker.sendBundle(bundleParams) | ||
``` |
@@ -1,2 +0,3 @@ | ||
import { AxiosError } from 'axios'; | ||
import { AxiosError } from 'axios' | ||
import { StreamEvent } from './api/interfaces' | ||
@@ -10,2 +11,10 @@ class MatchmakerError extends Error { | ||
export class JsonRpcError extends MatchmakerError { | ||
constructor(error: {code: number, message: string}) { | ||
super(`${error.code}: ${error.message}`) | ||
this.name = `JsonRpcError: ${error.code}` | ||
this.message = error.message | ||
} | ||
} | ||
export class NetworkFailure extends MatchmakerError { | ||
@@ -20,4 +29,4 @@ constructor(e: AxiosError) { | ||
export class UnimplementedNetwork extends MatchmakerError { | ||
constructor(network: {chainId: number, name: string}) { | ||
super(`Unimplemented network: ${JSON.stringify(network)}`) | ||
constructor({chainId}: {chainId: number}) { | ||
super(`Cannot infer network params from chainId: ${chainId}`) | ||
this.name = "UnimplementedNetwork" | ||
@@ -27,2 +36,9 @@ } | ||
export class UnimplementedStreamEvent extends MatchmakerError { | ||
constructor(eventType: StreamEvent) { | ||
super(`Unimplemented stream event type: ${eventType.toString()}`) | ||
this.name = "UnimplementedStreamEvent" | ||
} | ||
} | ||
export default MatchmakerError |
@@ -11,4 +11,3 @@ import { JsonRpcProvider, Network, Wallet } from 'ethers' | ||
const authSigner = new Wallet(Env.authKey) | ||
const network = {chainId: 5, name: "goerli"} | ||
const matchmaker = new Matchmaker(authSigner, network) | ||
const matchmaker = Matchmaker.useEthereumGoerli(authSigner) | ||
const wallet = new Wallet(Env.senderKey) | ||
@@ -15,0 +14,0 @@ const feeData = await provider.getFeeData() |
import { hexlify, toBigInt, toUtf8Bytes } from 'ethers' | ||
import { JsonRpcProvider, TransactionRequest } from 'ethers/types/providers' | ||
import { HintPreferences } from '../../api' | ||
import { HintPreferences } from '../..' | ||
import { initExample } from './helpers' | ||
@@ -37,6 +37,6 @@ | ||
const {matchmaker, signedTx} = await setupTxExample(provider, tip) | ||
const res = await matchmaker.sendShareTransaction(signedTx, | ||
const res = await matchmaker.sendTransaction(signedTx, | ||
{hints, maxBlockNumber} | ||
) | ||
console.debug("send private tx result", res) | ||
console.debug("sent tx", res) | ||
} |
@@ -5,8 +5,7 @@ import { JsonRpcProvider, keccak256 } from 'ethers' | ||
// lib | ||
import Matchmaker from '..' | ||
import { ShareBundleParams, PendingShareTransaction } from '../api' | ||
import Matchmaker, { BundleParams, IPendingTransaction, StreamEvent } from '..' | ||
import { getProvider, initExample } from './lib/helpers' | ||
import { sendTx, setupTxExample } from './lib/sendTx' | ||
const NUM_TARGET_BLOCKS = 5 | ||
const NUM_TARGET_BLOCKS = 3 | ||
@@ -16,4 +15,4 @@ /** | ||
*/ | ||
const sendTestBackrunBundle = async (provider: JsonRpcProvider, pendingTx: PendingShareTransaction, matchmaker: Matchmaker, targetBlock: number) => { | ||
// send ofa bundle w/ (basefee + 100)gwei gas fee | ||
const sendTestBackrunBundle = async (provider: JsonRpcProvider, pendingTx: IPendingTransaction, matchmaker: Matchmaker, targetBlock: number) => { | ||
// send bundle w/ (basefee + 100)gwei gas fee | ||
const {tx, wallet} = await setupTxExample(provider, BigInt(1e9) * BigInt(1e2), "im backrunniiiiing") | ||
@@ -24,19 +23,18 @@ const backrunTx = { | ||
} | ||
const backrun = [await wallet.signTransaction(backrunTx)] | ||
const shareTxs = [pendingTx.txHash] | ||
const backrunResults = [] | ||
const bundle = [ | ||
{hash: pendingTx.hash}, | ||
{tx: await wallet.signTransaction(backrunTx), canRevert: false}, | ||
] | ||
console.log(`sending backrun bundles targeting next ${NUM_TARGET_BLOCKS} blocks...`) | ||
for (let i = 0; i < NUM_TARGET_BLOCKS; i++) { | ||
const params: ShareBundleParams = { | ||
targetBlock: targetBlock + i, | ||
backrun, | ||
shareTxs, | ||
} | ||
const backrunRes = matchmaker.sendShareBundle(params) | ||
console.debug("sent share bundle", JSON.stringify(params)) | ||
backrunResults.push(backrunRes) | ||
const params: BundleParams = { | ||
inclusion: { | ||
block: targetBlock, | ||
maxBlock: targetBlock + NUM_TARGET_BLOCKS, | ||
}, | ||
body: bundle, | ||
} | ||
const backrunResult = await matchmaker.sendBundle(params) | ||
return { | ||
backrun, | ||
backrunResults: await Promise.all(backrunResults), | ||
bundle, | ||
backrunResult, | ||
} | ||
@@ -47,3 +45,3 @@ } | ||
const handleBackrun = async ( | ||
pendingTx: PendingShareTransaction, | ||
pendingTx: IPendingTransaction, | ||
provider: JsonRpcProvider, | ||
@@ -55,4 +53,7 @@ matchmaker: Matchmaker, | ||
const targetBlock = await provider.getBlockNumber() + 2 | ||
const { backrun, backrunResults } = await sendTestBackrunBundle(provider, pendingTx, matchmaker, targetBlock) | ||
console.log("backrun results", backrunResults) | ||
const { | ||
bundle, | ||
backrunResult, | ||
} = await sendTestBackrunBundle(provider, pendingTx, matchmaker, targetBlock) | ||
console.log("backrun result", backrunResult) | ||
@@ -65,4 +66,4 @@ // watch future blocks for backrun tx inclusion | ||
} | ||
console.log(`tx ${pendingTx.txHash} waiting for block`, targetBlock + i) | ||
// poll until block is available | ||
console.log(`tx ${pendingTx.hash} waiting for block`, targetBlock + i) | ||
// stall until target block is available | ||
while (await provider.getBlockNumber() < targetBlock + i) { | ||
@@ -73,6 +74,6 @@ await new Promise(resolve => setTimeout(resolve, 2000)) | ||
// check for inclusion of backrun tx in target block | ||
const backrunTxHash = keccak256(backrun[0]) | ||
const receipt = await provider.getTransactionReceipt(backrunTxHash) | ||
const checkTxHash = keccak256(bundle[1].tx!) | ||
const receipt = await provider.getTransactionReceipt(checkTxHash) | ||
if (receipt?.status === 1) { | ||
console.log("backrun tx included!") | ||
console.log(`bundle included! (found tx ${receipt.hash})`) | ||
// release mutex so the main thread can exit | ||
@@ -82,3 +83,3 @@ pendingMutex.release() | ||
} else { | ||
console.warn(`tx ${backrunTxHash} not included in block ${targetBlock}`) | ||
console.warn(`backrun tx ${checkTxHash} not included in block ${targetBlock}`) | ||
} | ||
@@ -101,3 +102,3 @@ } | ||
// listen for txs | ||
const txHandler = matchmaker.onShareTransaction(pendingTx => handleBackrun(pendingTx, provider, matchmaker, pendingMutex)) | ||
const txHandler = matchmaker.on(StreamEvent.Transaction, (pendingTx: IPendingTransaction) => handleBackrun(pendingTx, provider, matchmaker, pendingMutex)) | ||
console.log("listening for transactions...") | ||
@@ -112,3 +113,3 @@ | ||
// will block until the handler releases the mutex | ||
// will block until one of the handlers releases the mutex | ||
await pendingMutex.acquire() | ||
@@ -115,0 +116,0 @@ pendingMutex.release() |
@@ -1,2 +0,2 @@ | ||
import { HintPreferences } from '../api' | ||
import { HintPreferences } from '..' | ||
import { getProvider } from './lib/helpers' | ||
@@ -3,0 +3,0 @@ import { sendTx } from './lib/sendTx' |
@@ -5,3 +5,4 @@ import { id as ethersId, Wallet } from "ethers" | ||
id: number, | ||
result: any, | ||
result?: any, | ||
error?: {code: number, message: string}, | ||
jsonrpc: string, | ||
@@ -8,0 +9,0 @@ } |
117
src/index.ts
@@ -1,111 +0,14 @@ | ||
import axios, { AxiosError } from "axios" | ||
import { Wallet } from 'ethers' | ||
import EventSource from "eventsource" | ||
import { NetworkFailure, UnimplementedNetwork } from './error' | ||
import Matchmaker from './matchmaker' | ||
import { getRpcRequest, JsonRpcData } from './flashbots'; | ||
import { mungeShareBundleParams, mungePrivateTxParams, ShareBundleParams, PendingShareTransaction, ShareTransactionOptions } from './api' | ||
export { | ||
BundleParams, | ||
HintPreferences, | ||
IPendingBundle, | ||
IPendingTransaction, | ||
TransactionOptions, | ||
StreamEvent, | ||
} from "./api/interfaces" | ||
const bundleApiUrls = { | ||
goerli: "https://relay-goerli.flashbots.net", | ||
} | ||
export {SupportedNetworks} from "./api/networks" | ||
const streamingUrls = { | ||
goerli: "https://mev-share-goerli.flashbots.net", | ||
} | ||
class Matchmaker { | ||
private apiUrl?: string | ||
private streamUrl?: string | ||
constructor( | ||
private authSigner: Wallet, | ||
private network: { | ||
chainId: number, | ||
name: string, | ||
}, | ||
mevShareOptions?: { | ||
apiUrl?: string, | ||
streamUrl?: string, | ||
} | ||
) { | ||
if (network.chainId !== 5) { | ||
throw new UnimplementedNetwork(network) | ||
} | ||
this.authSigner = authSigner | ||
this.network = network | ||
this.apiUrl = mevShareOptions?.apiUrl || Object.entries(bundleApiUrls).find(kv => kv[0] === network.name.toLowerCase())?.[1] | ||
this.streamUrl = mevShareOptions?.streamUrl || Object.entries(streamingUrls).find(kv => kv[0] === network.name.toLowerCase())?.[1] | ||
} | ||
private async handleBundleApiRequest({ headers, body }: { headers: any, body: any}) { | ||
if (!this.apiUrl) throw new UnimplementedNetwork(this.network) | ||
try { | ||
const res = await axios.post(this.apiUrl, body, { | ||
headers | ||
}) | ||
return (res.data as JsonRpcData).result | ||
} catch (e) { | ||
console.debug(JSON.stringify(body)) | ||
throw new NetworkFailure(e as AxiosError) | ||
} | ||
} | ||
/** | ||
* Registers the provided callback to be called when a new mev-share transaction is received. | ||
* @param callback Async function to process pending tx. | ||
* @returns Event listener, which can be used to close the connection. | ||
* | ||
* @deprecated Use {@link onShareTransaction} instead. | ||
*/ | ||
public listenForShareTransactions(callback: (data: PendingShareTransaction) => void): EventSource { | ||
if (!this.streamUrl) throw new UnimplementedNetwork(this.network) | ||
const events = new EventSource(this.streamUrl) | ||
events.onmessage = (event) => { | ||
try { | ||
callback(JSON.parse(event.data) as PendingShareTransaction) | ||
} catch (e) { | ||
throw new NetworkFailure(e as AxiosError) | ||
} | ||
} | ||
return events | ||
} | ||
/** | ||
* Registers the provided callback to be called when a new mev-share transaction is received. | ||
* @param callback Async function to process pending tx. | ||
* @returns Event listener, which can be used to close the connection. | ||
*/ | ||
public onShareTransaction(callback: (data: PendingShareTransaction) => void): EventSource { | ||
return this.listenForShareTransactions(callback) | ||
} | ||
/** Sends a private transaction with MEV hints to mev-share. | ||
* | ||
* - Calls `eth_sendPrivateTransaction` with hints. | ||
* @param signedTx Signed transaction to send. | ||
* @param options Tx preferences; hints & block range. | ||
* @returns Transaction hash. | ||
*/ | ||
public async sendShareTransaction( | ||
signedTx: string, | ||
options?: ShareTransactionOptions, | ||
): Promise<string> { | ||
const params = mungePrivateTxParams(signedTx, options) | ||
const payload = await getRpcRequest(params, "eth_sendPrivateTransaction", this.authSigner) | ||
return await this.handleBundleApiRequest(payload) | ||
} | ||
/** Sends a Share bundle to mev-share. | ||
* @param bundleParams Parameters for the Share bundle. | ||
* @returns Array of bundle hashes. | ||
*/ | ||
public async sendShareBundle(bundleParams: ShareBundleParams): Promise<string[]> { | ||
const params = mungeShareBundleParams(bundleParams) | ||
const payload = await getRpcRequest(params, "eth_sendShareBundle", this.authSigner) | ||
return await this.handleBundleApiRequest(payload) | ||
} | ||
} | ||
export type { ShareBundleParams, PendingShareTransaction, ShareTransactionOptions } | ||
export default Matchmaker |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
118337
9
81
1957
185
7
1
Updatedethers@^6.3.0