├── .github └── workflows │ ├── main.yml │ └── size.yml ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src └── index.ts ├── tsconfig.json └── yarn.lock /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['10.x', '12.x', '14.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Test 29 | run: yarn test --ci --coverage --maxWorkers=2 30 | 31 | - name: Build 32 | run: yarn build 33 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nick Johnson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flashbots Proxy 2 | 3 | This package provides an easy way to interactively build and submit flashbots bundles. In conjunction with an archive node, it presents an RPC endpoint that transparently forks the target network and accumulates transactions, before allowing you to submit them all as a batch to a Flashbots endpoint, or revert them all and start from scratch. 4 | 5 | ## Usage 6 | 7 | ``` 8 | git clone https://github.com/arachnid/flashbots-proxy.git 9 | cd flashbots-proxy 10 | yarn 11 | yarn start [args] 12 | ``` 13 | 14 | Available command line arguments: 15 | 16 | ``` 17 | Options: 18 | -r --rpc RPC URL to proxy to (default: "http://localhost:8545/") 19 | -p --port Port number to listen on (default: "9545") 20 | -h, --help display help for command 21 | ``` 22 | 23 | Once started, connect your wallet (eg, MetaMask) to the endpoint exposed by the proxy (by default, `http://localhost:9545/`), and interact with apps and contracts normally. When you send your first transaction, flashbots-proxy will transparently create a new Ganache fork, and submit your transaction to it. At this point, your view of the chain is frozen in time at the point where you sent your first transaction, with only your own changes visible. 24 | 25 | As you send transactions, they will be displayed on the console like so: 26 | 27 | ``` 28 | 1. 0xE0b604208176C9c991A310E242677dfFddd3ab4D -> 0x283Af0B28c62C092C9727F1Ee09c02CA627EB7F5 29 | 2. 0xE0b604208176C9c991A310E242677dfFddd3ab4D -> 0x283Af0B28c62C092C9727F1Ee09c02CA627EB7F5 30 | (S)ubmit, (R)evert? 31 | ``` 32 | 33 | At any time, you can make another transaction, or enter 's' to submit the transactions as a bundle, or 'r' to revert. 34 | 35 | Submitting will cause the command to send the accumulated transactions to Flashbots as a bundle for inclusion in the next block, and wait for it to be mined before returning. This can fail - see [Flashbots' article on bundle troubleshooting](https://docs.flashbots.net/flashbots-auction/searchers/advanced/troubleshooting) for why - in which case you can try again as many times as you wish. 36 | 37 | Reverting will delete the Ganache fork, discard the transactions, and return you to a live view of the blockchain. After reverting, be sure to 'reset' your wallets so they do not have an out-of-date view of your accounts; in MetaMask you can do this by going to Settings -> Advanced -> Reset Account. You will need to repeat this for each account. 38 | 39 | ## Disclaimer 40 | 41 | This code is brand new, pretty manky, and almost certainly contains errors. I take no responsibility for any damage it causes, and you use it entirely at your own risk. 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "files": [ 7 | "dist", 8 | "src" 9 | ], 10 | "engines": { 11 | "node": ">=10" 12 | }, 13 | "scripts": { 14 | "watch": "tsdx watch", 15 | "build": "tsdx build", 16 | "start": "tsdx build && node dist/index.js", 17 | "test": "tsdx test", 18 | "lint": "tsdx lint", 19 | "prepare": "tsdx build", 20 | "size": "size-limit", 21 | "analyze": "size-limit --why" 22 | }, 23 | "peerDependencies": {}, 24 | "husky": { 25 | "hooks": { 26 | "pre-commit": "tsdx lint" 27 | } 28 | }, 29 | "prettier": { 30 | "printWidth": 80, 31 | "semi": true, 32 | "singleQuote": true, 33 | "trailingComma": "es5" 34 | }, 35 | "name": "flashbots-proxy", 36 | "author": "Nick Johnson", 37 | "module": "dist/flashbots-proxy.esm.js", 38 | "size-limit": [ 39 | { 40 | "path": "dist/flashbots-proxy.cjs.production.min.js", 41 | "limit": "10 KB" 42 | }, 43 | { 44 | "path": "dist/flashbots-proxy.esm.js", 45 | "limit": "10 KB" 46 | } 47 | ], 48 | "devDependencies": { 49 | "@size-limit/preset-small-lib": "^7.0.8", 50 | "@types/express": "^4.17.13", 51 | "husky": "^7.0.4", 52 | "size-limit": "^7.0.8", 53 | "tsdx": "^0.14.1", 54 | "tslib": "^2.3.1", 55 | "typescript": "^4.5.5" 56 | }, 57 | "dependencies": { 58 | "@flashbots/ethers-provider-bundle": "^0.5.0", 59 | "@types/cors": "^2.8.12", 60 | "commander": "^9.0.0", 61 | "cors": "^2.8.5", 62 | "ethers": "^5.6.9", 63 | "express": "^4.17.2", 64 | "ganache": "^7.4.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import ethers from 'ethers'; 3 | import { FlashbotsBundleProvider, FlashbotsBundleResolution, FlashbotsTransactionResponse, RelayResponseError, SimulationResponseSuccess } from '@flashbots/ethers-provider-bundle'; 4 | import express from 'express'; 5 | import ganache from 'ganache'; 6 | import readline from 'readline'; 7 | import cors from "cors"; 8 | 9 | const program = new Command(); 10 | program 11 | .option('-r --rpc ', 'RPC URL to proxy to', 'http://localhost:8545/') 12 | .option('-p --port ', 'Port number to listen on', '9545'); 13 | 14 | program.parse(process.argv); 15 | 16 | const options = program.opts(); 17 | 18 | const GANACHE_CONFIG = { 19 | chain: { 20 | chainId: 1, 21 | }, 22 | fork: { 23 | url: options.rpc, 24 | }, 25 | miner: { 26 | blockTime: 1 27 | }, 28 | logging: { 29 | logger: { 30 | log: () => {} 31 | } 32 | } 33 | } 34 | 35 | let ac = new AbortController(); 36 | 37 | const rl = readline.createInterface({ 38 | input: process.stdin, 39 | output: process.stdout 40 | }); 41 | 42 | const provider = new ethers.providers.JsonRpcProvider({ url: options.rpc }); 43 | const authSigner = new ethers.Wallet(ethers.utils.randomBytes(32)); 44 | 45 | function isRelayResponseError(r: FlashbotsTransactionResponse | SimulationResponseSuccess | RelayResponseError): r is RelayResponseError { 46 | return (r as any)?.error !== undefined; 47 | } 48 | 49 | class BundleProxy { 50 | baseProvider: ethers.providers.JsonRpcProvider; 51 | provider: ethers.providers.JsonRpcProvider; 52 | bundle: ethers.Transaction[]|undefined; 53 | 54 | constructor(provider: ethers.providers.JsonRpcProvider) { 55 | this.baseProvider = provider; 56 | this.provider = provider; 57 | } 58 | 59 | async rpcHandler(method: string, params: any): Promise { 60 | switch(method) { 61 | case 'eth_sendRawTransaction': 62 | return this.transactionHandler(params as any[]); 63 | default: 64 | return this.provider.send(method, params); 65 | } 66 | } 67 | 68 | private async transactionHandler(params: any[]) { 69 | const tx = ethers.utils.parseTransaction(params[0] as string); 70 | const bundle = await this.getBundle(); 71 | for(let i = bundle.length - 1; i >= 0; i--) { 72 | if(bundle[i].from === tx.from && bundle[i].nonce === tx.nonce && bundle[i].hash !== tx.hash) { 73 | if(i < bundle.length - 1) { 74 | console.log("Warning: Replacing TX from earlier in the bundle"); 75 | } 76 | bundle[i] = tx; 77 | return this.provider.send('eth_sendRawTransaction', params); 78 | } 79 | } 80 | bundle.push(tx); 81 | showPrompt(); 82 | return this.provider.send('eth_sendRawTransaction', params); 83 | } 84 | 85 | async getBundle(): Promise { 86 | if(this.bundle === undefined) { 87 | const options = Object.assign({}, GANACHE_CONFIG); 88 | options.chain.chainId = this.baseProvider.network.chainId; 89 | this.provider = new ethers.providers.Web3Provider(ganache.provider(options) as any); 90 | console.log(`Created fork at block ${await this.provider.getBlockNumber()}`); 91 | this.bundle = []; 92 | } 93 | return this.bundle; 94 | } 95 | 96 | revertFork() { 97 | this.bundle = undefined; 98 | this.provider = this.baseProvider; 99 | } 100 | 101 | async submitFork(): Promise { 102 | if(this.bundle === undefined) { 103 | return false; 104 | } 105 | const blockno = 1 + await this.baseProvider.getBlockNumber(); 106 | console.log(`Attempting to submit bundle at block number ${blockno}`); 107 | const flashbotsProvider = await FlashbotsBundleProvider.create(this.baseProvider, authSigner); 108 | const txresponse = await flashbotsProvider.sendBundle(this.bundle.map((tx) => ({ 109 | signedTransaction: ethers.utils.serializeTransaction( 110 | tx, 111 | {r: tx.r as string, s: tx.s as string, v: tx.v as number}) 112 | })), blockno); 113 | 114 | if(isRelayResponseError(txresponse)) { 115 | console.log(`Error submitting bundle: ${txresponse.error.message}`); 116 | return false; 117 | } 118 | 119 | const sim = await txresponse.simulate(); 120 | if(isRelayResponseError(sim)) { 121 | console.log(`Simulation produced an error: ${sim.error}`); 122 | } else { 123 | console.log(`Simulation result: ${sim.firstRevert === undefined ? 'success': 'failure'}`) 124 | } 125 | 126 | const status = await txresponse.wait(); 127 | switch(status) { 128 | case FlashbotsBundleResolution.BundleIncluded: 129 | console.log("Bundle mined!"); 130 | this.revertFork(); 131 | return true; 132 | case FlashbotsBundleResolution.AccountNonceTooHigh: 133 | console.log("Failed to mine bundle: account nonce too high. Resetting fork."); 134 | this.revertFork(); 135 | return true; 136 | case FlashbotsBundleResolution.BlockPassedWithoutInclusion: 137 | console.log("Failed to include bundle in block; try again."); 138 | return false; 139 | } 140 | } 141 | } 142 | 143 | const proxy = new BundleProxy(provider); 144 | const app = express(); 145 | app.use(express.json()); 146 | app.use(cors()); 147 | 148 | app.post('/', async (req, res) => { 149 | const { id, method, params } = req.body; 150 | try { 151 | const result = await proxy.rpcHandler(method, params); 152 | res.json({ 153 | jsonrpc: "2.0", 154 | id, 155 | result 156 | }); 157 | } catch(e) { 158 | if((e as any)?.body !== undefined) { 159 | const { error } = JSON.parse((e as any).body); 160 | res.json({ 161 | jsonrpc: "2.0", 162 | id, 163 | error 164 | }); 165 | } else { 166 | console.log(e); 167 | } 168 | } 169 | }); 170 | 171 | app.listen(parseInt(options.port), () => { 172 | console.log(`Listening on port ${options.port}`); 173 | }); 174 | 175 | function showPrompt() { 176 | ac.abort(); 177 | if(!proxy.bundle) { 178 | return; 179 | } 180 | for(let i = 0; i < proxy.bundle?.length; i++) { 181 | const tx = proxy.bundle[i]; 182 | console.log(`${i + 1}. ${tx.from} -> ${tx.to}`); 183 | } 184 | ac = new AbortController(); 185 | rl.question('(S)ubmit, (R)evert?', {signal: ac.signal}, handleResponse); 186 | } 187 | 188 | async function handleResponse(response: string) { 189 | switch(response.toLowerCase()) { 190 | case 's': 191 | const result = await proxy.submitFork(); 192 | if(!result) { 193 | showPrompt(); 194 | } 195 | break; 196 | case 'r': 197 | console.log("Reverting."); 198 | proxy.revertFork(); 199 | break; 200 | default: 201 | showPrompt(); 202 | break; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "target": "ES2020", 7 | "lib": ["dom", "esnext"], 8 | "importHelpers": true, 9 | // output .d.ts declaration files for consumers 10 | "declaration": true, 11 | // output .js.map sourcemap files for consumers 12 | "sourceMap": true, 13 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 14 | "rootDir": "./src", 15 | // stricter type-checking for stronger correctness. Recommended by TS 16 | "strict": true, 17 | // linter checks for common issues 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | // use Node's module resolution algorithm, instead of the legacy TS one 24 | "moduleResolution": "node", 25 | // transpile JSX to React.createElement 26 | "jsx": "react", 27 | // interop between ESM and CJS modules. Recommended by TS 28 | "esModuleInterop": true, 29 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 30 | "skipLibCheck": true, 31 | // error out if import and file system have a casing mismatch. Recommended by TS 32 | "forceConsistentCasingInFileNames": true, 33 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 34 | "noEmit": true, 35 | } 36 | } 37 | --------------------------------------------------------------------------------