diff --git a/.github/badges/branches.svg b/.github/badges/branches.svg
new file mode 100644
index 0000000..f97e335
--- /dev/null
+++ b/.github/badges/branches.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.github/badges/jacoco.svg b/.github/badges/jacoco.svg
new file mode 100644
index 0000000..45c51d7
--- /dev/null
+++ b/.github/badges/jacoco.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..6efc9c6
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,93 @@
+name: mev-share-java CI
+
+on:
+ push:
+ branches: [ "main" ]
+
+jobs:
+ macos:
+ name: macOS
+ runs-on: [ macos-latest ]
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: 17
+ distribution: 'temurin'
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v2
+
+ - name: Execute Gradle build
+ env:
+ SIGNER_PRIVATE_KEY: ${{ secrets.SIGNER_PRIVATE_KEY }}
+ GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }}
+ run: ./gradlew build
+
+ windows:
+ name: windows
+ runs-on: [ windows-latest ]
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: 17
+ distribution: 'temurin'
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v2
+
+ - name: Execute Gradle build
+ env:
+ SIGNER_PRIVATE_KEY: ${{ secrets.SIGNER_PRIVATE_KEY }}
+ GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }}
+ run: ./gradlew build
+
+ linux:
+ name: linux
+ runs-on: [ ubuntu-latest ]
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: 17
+ distribution: 'temurin'
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v2
+
+ - name: Execute Gradle build
+ env:
+ SIGNER_PRIVATE_KEY: ${{ secrets.SIGNER_PRIVATE_KEY }}
+ GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }}
+ run: ./gradlew build
+
+ - name: Generate JaCoCo Badge
+ uses: cicirello/jacoco-badge-generator@v2
+ with:
+ generate-branches-badge: true
+ jacoco-csv-file: build/reports/jacoco/test/jacocoTestReport.csv
+
+ - name: Log coverage percentage
+ run: |
+ echo "coverage = ${{ steps.jacoco.outputs.coverage }}"
+ echo "branch coverage = ${{ steps.jacoco.outputs.branches }}"
+
+ - name: Commit and push the badge (if it changed)
+ uses: EndBug/add-and-commit@v7
+ with:
+ default_author: github_actor
+ message: 'commit badge'
+ add: '*.svg'
+
+ - name: Upload JaCoCo coverage report
+ uses: actions/upload-artifact@v2
+ with:
+ name: jacoco-report
+ path: build/reports/jacoco/
\ No newline at end of file
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
new file mode 100644
index 0000000..614dd50
--- /dev/null
+++ b/.github/workflows/pull_request.yml
@@ -0,0 +1,85 @@
+name: mev-share-java pull request CI
+
+on:
+ pull_request:
+
+jobs:
+ macos:
+ name: macOS
+ runs-on: [ macos-latest ]
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: 17
+ distribution: 'temurin'
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v2
+
+ - name: Execute Gradle build
+ env:
+ SIGNER_PRIVATE_KEY: ${{ secrets.SIGNER_PRIVATE_KEY }}
+ GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }}
+ run: ./gradlew build
+
+ windows:
+ name: windows
+ runs-on: [ windows-latest ]
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: 17
+ distribution: 'temurin'
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v2
+
+ - name: Execute Gradle build
+ env:
+ SIGNER_PRIVATE_KEY: ${{ secrets.SIGNER_PRIVATE_KEY }}
+ GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }}
+ run: ./gradlew build
+
+ linux:
+ name: linux
+ runs-on: [ ubuntu-latest ]
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: 17
+ distribution: 'temurin'
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v2
+
+ - name: Execute Gradle build
+ env:
+ SIGNER_PRIVATE_KEY: ${{ secrets.SIGNER_PRIVATE_KEY }}
+ GOERLI_RPC_URL: ${{ secrets.GOERLI_RPC_URL }}
+ run: ./gradlew build
+
+ - name: Generate JaCoCo Badge
+ uses: cicirello/jacoco-badge-generator@v2
+ with:
+ generate-branches-badge: true
+ jacoco-csv-file: build/reports/jacoco/test/jacocoTestReport.csv
+
+ - name: Log coverage percentage
+ run: |
+ echo "coverage = ${{ steps.jacoco.outputs.coverage }}"
+ echo "branch coverage = ${{ steps.jacoco.outputs.branches }}"
+
+ - name: Upload JaCoCo coverage report
+ uses: actions/upload-artifact@v2
+ with:
+ name: jacoco-report
+ path: build/reports/jacoco/
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..9557754
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,56 @@
+name: Publish package to the Maven Central Repository and GitHub Packages
+on:
+ release:
+ types: [created]
+jobs:
+ sonatype-publish:
+ runs-on: ubuntu-latest
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.OSSRH_USERNAME }}
+ ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.OSSRH_TOKEN }}
+ ORG_GRADLE_PROJECT_signingKey: ${{ secrets.GPG_SIGN_KEY }}
+ ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.GPG_SIGN_PW }}
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
+ - name: Set up JDK 8
+ uses: actions/setup-java@v3
+ with:
+ java-version: 8
+ distribution: 'temurin'
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v2.4.2
+
+ - name: Publish package
+ run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository
+
+
+ github-publish:
+ runs-on: ubuntu-latest
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ ORG_GRADLE_PROJECT_signingKey: ${{ secrets.GPG_SIGN_KEY }}
+ ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.GPG_SIGN_PW }}
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
+ - name: Set up JDK 8
+ uses: actions/setup-java@v3
+ with:
+ java-version: 8
+ distribution: 'temurin'
+
+ - name: Setup Gradle
+ uses: gradle/gradle-build-action@v2.4.2
+
+ - name: Publish package
+ run: ./gradlew publishAllPublicationsToGitHubPackagesRepository
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..df54656
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,43 @@
+.gradle
+build/
+!gradle/wrapper/gradle-wrapper.jar
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### IntelliJ IDEA ###
+.idea/
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5b8ab79
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 optimism-java
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..57697a0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+[![mev-share-java CI](https://github.com/optimism-java/mev-share-java/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/optimism-java/mev-share-java/actions/workflows/build.yml)
+[![License](https://img.shields.io/badge/license-MIT-blue)](https://opensource.org/licenses/MIT)
+![Coverage](.github/badges/jacoco.svg)
+![Branches](.github/badges/branches.svg)
+# mev-share-java
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..4e474bf
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,131 @@
+plugins {
+ id 'java-library'
+ id 'jacoco'
+ id 'com.diffplug.spotless' version '6.20.0'
+ id 'maven-publish'
+ id "io.github.gradle-nexus.publish-plugin" version "1.3.0"
+ id 'signing'
+ id "net.ltgt.errorprone" version "3.1.0"
+}
+
+group = 'me.grapebaba'
+version = '0.1.0-SNAPSHOT'
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation 'com.squareup.okhttp3:okhttp:4.11.0'
+ implementation 'com.squareup.okhttp3:okhttp-sse:4.11.0'
+
+ api 'org.web3j:core:4.10.2'
+
+ implementation 'org.slf4j:slf4j-api:2.0.7'
+ implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
+
+ implementation 'org.apache.commons:commons-lang3:3.13.0'
+ errorprone("com.google.errorprone:error_prone_core:2.18.0")
+
+ testImplementation platform('org.junit:junit-bom:5.9.1')
+ testImplementation 'org.junit.jupiter:junit-jupiter'
+ testImplementation 'org.apache.logging.log4j:log4j-slf4j2-impl:2.20.0'
+}
+
+sourceSets.test.java {
+ srcDirs += "$projectDir/example"
+}
+
+test {
+ useJUnitPlatform()
+ testLogging {
+ events "passed", "skipped", "failed"
+ }
+ finalizedBy jacocoTestReport
+}
+
+jacocoTestReport {
+ dependsOn test
+
+ afterEvaluate {
+ classDirectories.setFrom(files(classDirectories.files.collect {
+ fileTree(dir: it, exclude: [
+ "net/flashbots/models/**"
+ ])
+ }))
+ }
+
+ reports {
+ csv.required = true
+ }
+}
+
+jacocoTestCoverageVerification {
+ afterEvaluate {
+ classDirectories.setFrom(files(classDirectories.files.collect {
+ fileTree(dir: it, exclude: [
+ "net/flashbots/models/**"
+ ])
+ }))
+ }
+
+ violationRules {
+ rule {
+ limit {
+ minimum = 0.5
+ }
+ }
+ }
+}
+
+check {
+ dependsOn += jacocoTestCoverageVerification
+}
+
+tasks.withType(Test).configureEach {
+ def outputDir = reports.junitXml.outputLocation
+ jvmArgumentProviders << ({
+ [
+ "-Djunit.platform.reporting.open.xml.enabled=true",
+ "-Djunit.platform.reporting.output.dir=${outputDir.get().asFile.absolutePath}"
+ ]
+ } as CommandLineArgumentProvider)
+}
+
+javadoc {
+ if (JavaVersion.current().isJava9Compatible()) {
+ options.addBooleanOption('html5', true)
+ }
+}
+
+java {
+ withJavadocJar()
+ withSourcesJar()
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(17)
+ }
+}
+
+spotless {
+ format 'misc', {
+ // define the files to apply `misc` to
+ target '*.gradle', '*.md', '.gitignore'
+
+ // define the steps to apply to those files
+ trimTrailingWhitespace()
+ indentWithTabs() // or spaces. Takes an integer argument if you don't like 4
+ endWithNewline()
+ }
+ java {
+
+ palantirJavaFormat()
+ formatAnnotations()
+ importOrder('java|javax', '\\#')
+ removeUnusedImports()
+ }
+}
+
+tasks.register('printSourceDirs') {
+ print("$projectDir\n")
+ print(sourceSets)
+}
diff --git a/example/bundle/RpcMevSendBundle.java b/example/bundle/RpcMevSendBundle.java
new file mode 100644
index 0000000..43c771b
--- /dev/null
+++ b/example/bundle/RpcMevSendBundle.java
@@ -0,0 +1,85 @@
+package bundle;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import io.reactivex.disposables.Disposable;
+import net.flashbots.MevShareClient;
+import net.flashbots.models.bundle.BundleItemType;
+import net.flashbots.models.bundle.BundleParams;
+import net.flashbots.models.bundle.Inclusion;
+import net.flashbots.models.bundle.SendBundleResponse;
+import net.flashbots.models.common.Network;
+import net.flashbots.models.event.MevShareEvent;
+import org.web3j.crypto.Credentials;
+import org.web3j.crypto.RawTransaction;
+import org.web3j.crypto.TransactionEncoder;
+import org.web3j.protocol.Web3j;
+import org.web3j.protocol.core.DefaultBlockParameterName;
+import org.web3j.protocol.http.HttpService;
+import org.web3j.tx.gas.DefaultGasProvider;
+import org.web3j.utils.Convert;
+import org.web3j.utils.Numeric;
+
+/**
+ * Rpc mev_sendBundle method example
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class RpcMevSendBundle {
+
+ public static void main(String[] args) throws ExecutionException, InterruptedException, IOException {
+ Credentials sender = Credentials.create("");
+ var web3j = Web3j.build(new HttpService(""));
+ var mevShareClient = new MevShareClient(Network.GOERLI, sender, web3j);
+
+ CompletableFuture future = new CompletableFuture<>();
+ Disposable eventSource = mevShareClient.subscribe(mevShareEvent -> {
+ if (mevShareEvent.getHash() != null) {
+ future.complete(mevShareEvent);
+ }
+ });
+ MevShareEvent mevShareEvent = future.get();
+ eventSource.dispose();
+
+ BigInteger number = web3j.ethGetBlockByNumber(DefaultBlockParameterName.LATEST, false)
+ .send()
+ .getBlock()
+ .getNumber();
+
+ Inclusion inclusion =
+ new Inclusion().setBlock(number.add(BigInteger.ONE)).setMaxBlock(number.add(BigInteger.valueOf(4)));
+
+ BundleItemType.HashItem bundleItem = new BundleItemType.HashItem().setHash(mevShareEvent.getHash());
+
+ Credentials signer = Credentials.create("");
+ BigInteger nonce = web3j.ethGetTransactionCount(signer.getAddress(), DefaultBlockParameterName.PENDING)
+ .send()
+ .getTransactionCount();
+ BigInteger gasPrice = web3j.ethGasPrice().send().getGasPrice();
+ BigInteger gasLimit = DefaultGasProvider.GAS_LIMIT;
+ final String to = "";
+ final String amount = "";
+ RawTransaction rawTransaction = RawTransaction.createEtherTransaction(
+ nonce,
+ gasPrice,
+ gasLimit,
+ to,
+ Convert.toWei(amount, Convert.Unit.ETHER).toBigInteger());
+ byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, Network.GOERLI.chainId(), signer);
+ String hexValue = Numeric.toHexString(signedMessage);
+
+ BundleItemType.TxItem txItem =
+ new BundleItemType.TxItem().setTx(hexValue).setCanRevert(true);
+
+ // body must include a tx
+ BundleParams bundleParams = new BundleParams().setInclusion(inclusion).setBody(List.of(bundleItem, txItem));
+
+ CompletableFuture res = mevShareClient.sendBundle(bundleParams);
+ System.out.println(res.get().getBundleHash());
+ }
+}
diff --git a/example/bundle/RpcMevSimBundle.java b/example/bundle/RpcMevSimBundle.java
new file mode 100644
index 0000000..cde2ee1
--- /dev/null
+++ b/example/bundle/RpcMevSimBundle.java
@@ -0,0 +1,80 @@
+package bundle;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+import net.flashbots.MevShareClient;
+import net.flashbots.models.bundle.BundleItemType;
+import net.flashbots.models.bundle.BundleParams;
+import net.flashbots.models.bundle.Inclusion;
+import net.flashbots.models.bundle.SimBundleOptions;
+import net.flashbots.models.common.Network;
+import org.web3j.crypto.Credentials;
+import org.web3j.crypto.RawTransaction;
+import org.web3j.crypto.TransactionEncoder;
+import org.web3j.protocol.Web3j;
+import org.web3j.protocol.core.DefaultBlockParameter;
+import org.web3j.protocol.core.DefaultBlockParameterName;
+import org.web3j.protocol.http.HttpService;
+import org.web3j.tx.gas.DefaultGasProvider;
+import org.web3j.utils.Convert;
+import org.web3j.utils.Numeric;
+
+/**
+ * Rpc mev_simBundle method example
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class RpcMevSimBundle {
+
+ public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
+
+ Credentials sender = Credentials.create("");
+ var web3j = Web3j.build(new HttpService(""));
+ var mevShareClient = new MevShareClient(Network.GOERLI, sender, web3j);
+
+ var latestBlock = web3j.ethGetBlockByNumber(DefaultBlockParameterName.LATEST, false)
+ .send()
+ .getBlock();
+ var parentBlock = web3j.ethGetBlockByNumber(
+ DefaultBlockParameter.valueOf(latestBlock.getNumber().subtract(BigInteger.ONE)), false)
+ .send()
+ .getBlock();
+
+ Inclusion inclusion = new Inclusion()
+ .setBlock(latestBlock.getNumber().subtract(BigInteger.ONE))
+ .setMaxBlock(latestBlock.getNumber().add(BigInteger.valueOf(10)));
+
+ Credentials signer = Credentials.create("");
+ BigInteger nonce = web3j.ethGetTransactionCount(signer.getAddress(), DefaultBlockParameterName.PENDING)
+ .send()
+ .getTransactionCount();
+ final String to = "";
+ RawTransaction rawTransaction = RawTransaction.createEtherTransaction(
+ nonce,
+ web3j.ethGasPrice().send().getGasPrice(),
+ DefaultGasProvider.GAS_LIMIT,
+ to,
+ Convert.toWei("0", Convert.Unit.ETHER).toBigInteger());
+ byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, Network.GOERLI.chainId(), signer);
+ String hexValue = Numeric.toHexString(signedMessage);
+
+ BundleItemType.TxItem bundleItem =
+ new BundleItemType.TxItem().setTx(hexValue).setCanRevert(true);
+
+ BundleParams bundleParams = new BundleParams().setInclusion(inclusion).setBody(List.of(bundleItem));
+ SimBundleOptions options = new SimBundleOptions()
+ .setParentBlock(latestBlock.getNumber().subtract(BigInteger.ONE))
+ .setBlockNumber(latestBlock.getNumber())
+ .setTimestamp(parentBlock.getTimestamp().add(BigInteger.valueOf(12)))
+ .setGasLimit(parentBlock.getGasLimit())
+ .setBaseFee(parentBlock.getBaseFeePerGas())
+ .setTimeout(30);
+
+ var res = mevShareClient.simBundle(bundleParams, options);
+ System.out.println(res.get().toString());
+ }
+}
diff --git a/example/bundle/RpcSendPrivateTx.java b/example/bundle/RpcSendPrivateTx.java
new file mode 100644
index 0000000..7a3d0bd
--- /dev/null
+++ b/example/bundle/RpcSendPrivateTx.java
@@ -0,0 +1,70 @@
+package bundle;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import net.flashbots.MevShareClient;
+import net.flashbots.models.bundle.HintPreferences;
+import net.flashbots.models.bundle.PrivateTxOptions;
+import net.flashbots.models.common.Network;
+import org.web3j.crypto.Credentials;
+import org.web3j.crypto.RawTransaction;
+import org.web3j.crypto.TransactionEncoder;
+import org.web3j.protocol.Web3j;
+import org.web3j.protocol.core.DefaultBlockParameterName;
+import org.web3j.protocol.core.methods.response.EthBlock;
+import org.web3j.protocol.http.HttpService;
+import org.web3j.utils.Convert;
+import org.web3j.utils.Numeric;
+
+/**
+ * Rpc eth_sendPrivateTransaction method example
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class RpcSendPrivateTx {
+
+ public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
+ Credentials sender = Credentials.create("");
+ var web3j = Web3j.build(new HttpService(""));
+ var mevShareClient = new MevShareClient(Network.GOERLI, sender, web3j);
+
+ EthBlock.Block latest = web3j.ethGetBlockByNumber(DefaultBlockParameterName.LATEST, false)
+ .send()
+ .getBlock();
+
+ BigInteger maxPriorityFeePerGas = BigInteger.valueOf(1_000_000_000L);
+
+ Credentials signer = Credentials.create("");
+ BigInteger nonce = web3j.ethGetTransactionCount(signer.getAddress(), DefaultBlockParameterName.PENDING)
+ .send()
+ .getTransactionCount();
+ final String to = "";
+
+ RawTransaction rawTransaction = RawTransaction.createTransaction(
+ 5L,
+ nonce,
+ latest.getGasLimit(),
+ to,
+ Convert.toWei("0", Convert.Unit.ETHER).toBigInteger(),
+ Numeric.toHexString("".getBytes(StandardCharsets.UTF_8)),
+ maxPriorityFeePerGas,
+ latest.getBaseFeePerGas().multiply(BigInteger.TWO).add(maxPriorityFeePerGas));
+ byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, Network.GOERLI.chainId(), signer);
+ String signRawTx = Numeric.toHexString(signedMessage);
+
+ PrivateTxOptions txOptions = new PrivateTxOptions()
+ .setHints(new HintPreferences()
+ .setCalldata(true)
+ .setContractAddress(true)
+ .setFunctionSelector(true)
+ .setLogs(true));
+
+ CompletableFuture res = mevShareClient.sendPrivateTransaction(signRawTx, txOptions);
+ System.out.println(res.get());
+ }
+}
diff --git a/example/event/SseHistorical.java b/example/event/SseHistorical.java
new file mode 100644
index 0000000..c0b4254
--- /dev/null
+++ b/example/event/SseHistorical.java
@@ -0,0 +1,36 @@
+package event;
+
+import java.math.BigInteger;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import net.flashbots.MevShareClient;
+import net.flashbots.models.common.Network;
+import net.flashbots.models.event.EventHistoryEntry;
+import net.flashbots.models.event.EventHistoryInfo;
+import net.flashbots.models.event.EventHistoryParams;
+
+/**
+ * SSE historical example
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class SseHistorical {
+
+ public static void main(String[] args) throws ExecutionException, InterruptedException {
+ var mevShareClient = new MevShareClient(Network.GOERLI, null, null);
+
+ // get event history info
+ CompletableFuture historyInfoFuture = mevShareClient.getEventHistoryInfo();
+ EventHistoryInfo eventHistoryInfo = historyInfoFuture.get();
+ System.out.println(eventHistoryInfo);
+
+ // get event history entry
+ var historyParams = new EventHistoryParams().setLimit(20).setBlockStart(BigInteger.valueOf(1_000_000L));
+ CompletableFuture> eventHistory = mevShareClient.getEventHistory(historyParams);
+ List eventHistoryEntries = eventHistory.get();
+ System.out.println(eventHistoryEntries);
+ }
+}
diff --git a/example/event/SseSubscribe.java b/example/event/SseSubscribe.java
new file mode 100644
index 0000000..9b0a7ae
--- /dev/null
+++ b/example/event/SseSubscribe.java
@@ -0,0 +1,29 @@
+package event;
+
+import java.util.function.Consumer;
+
+import io.reactivex.disposables.Disposable;
+import net.flashbots.MevShareClient;
+import net.flashbots.models.common.Network;
+import net.flashbots.models.event.MevShareEvent;
+
+/**
+ * SSE subscribe Example
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class SseSubscribe {
+
+ public static void main(String[] args) {
+ var mevShareClient = new MevShareClient(Network.GOERLI, null, null);
+ Consumer eventListener = mevShareEvent -> {
+ // do something and do not block here...
+ };
+
+ Disposable disposable = mevShareClient.subscribe(eventListener);
+
+ // remember to release when no longer to subscribe events
+ disposable.dispose();
+ }
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..249e583
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e69a35f
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Aug 15 11:39:15 CST 2023
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..1b6c787
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,234 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..1fd858f
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'mev-share-java'
diff --git a/src/main/java/net/flashbots/MevShareApi.java b/src/main/java/net/flashbots/MevShareApi.java
new file mode 100644
index 0000000..17d35e5
--- /dev/null
+++ b/src/main/java/net/flashbots/MevShareApi.java
@@ -0,0 +1,101 @@
+package net.flashbots;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+import io.reactivex.disposables.Disposable;
+import net.flashbots.models.bundle.BundleParams;
+import net.flashbots.models.bundle.PrivateTxOptions;
+import net.flashbots.models.bundle.SendBundleResponse;
+import net.flashbots.models.bundle.SimBundleOptions;
+import net.flashbots.models.bundle.SimBundleResponse;
+import net.flashbots.models.event.EventHistoryEntry;
+import net.flashbots.models.event.EventHistoryInfo;
+import net.flashbots.models.event.EventHistoryParams;
+import net.flashbots.models.event.MevShareEvent;
+
+/**
+ * The interface of Mev-Share API.
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public interface MevShareApi {
+
+ /**
+ * Gets event history info.
+ *
+ * @return the event history info {@link EventHistoryInfo}
+ */
+ CompletableFuture getEventHistoryInfo();
+
+ /**
+ * Gets the list of event history entries.
+ *
+ * @param params the event history params {@link EventHistoryParams}
+ * @return the list of event history entries {@link EventHistoryEntry}
+ */
+ CompletableFuture> getEventHistory(EventHistoryParams params);
+
+ /**
+ * Subscribe to All MEV-Share events with flowable.
+ *
+ * @param consumer the consumer for the event
+ * @return the disposable, call {@link Disposable#dispose()} to unsubscribe
+ */
+ Disposable subscribe(Consumer consumer);
+
+ /**
+ * Subscribe to Tx MEV-Share events with flowable.
+ *
+ * @param consumer the consumer
+ * @return the disposable
+ */
+ Disposable subscribeTx(Consumer consumer);
+
+ /**
+ * Subscribe to bundle MEV-Share events with flowable.
+ *
+ * @param consumer the consumer
+ * @return the disposable
+ */
+ Disposable subscribeBundle(Consumer consumer);
+
+ /**
+ * Send MEV-Share bundle to the network.
+ *
+ * @param params the bundle params {@link BundleParams}
+ * @return the bundle response {@link SendBundleResponse} otherwise return the error
+ */
+ CompletableFuture sendBundle(BundleParams params);
+
+ /**
+ * Simulate bundle
+ *
+ * @param params simulate bundle param instance
+ * @param options simulate bundle options instance
+ * @return the simulate bundle response {@link SimBundleResponse} otherwise return an error
+ */
+ CompletableFuture simBundle(BundleParams params, SimBundleOptions options);
+
+ /**
+ * Bundles containing pending transactions (specified by `{hash}` instead of `{tx}` in `params.body`) may
+ * only be simulated after those transactions have landed on chain. If the bundle contains
+ * pending transactions, this method will wait for the transactions to land before simulating.
+ *
+ * @param params simulate bundle param instance
+ * @param options simulate bundle options instance
+ * @return the simulate bundle response {@link SimBundleResponse} otherwise return an error
+ */
+ CompletableFuture simulateBundle(BundleParams params, SimBundleOptions options);
+
+ /**
+ * Send private transaction
+ *
+ * @param signedRawTx string of signed raw transaction
+ * @param options private transaction options
+ * @return the private transaction hash otherwise return an error
+ */
+ CompletableFuture sendPrivateTransaction(String signedRawTx, PrivateTxOptions options);
+}
diff --git a/src/main/java/net/flashbots/MevShareClient.java b/src/main/java/net/flashbots/MevShareClient.java
new file mode 100644
index 0000000..1a2a9c8
--- /dev/null
+++ b/src/main/java/net/flashbots/MevShareClient.java
@@ -0,0 +1,246 @@
+package net.flashbots;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.reactivex.BackpressureStrategy;
+import io.reactivex.Flowable;
+import io.reactivex.disposables.Disposable;
+import net.flashbots.common.MevShareApiException;
+import net.flashbots.common.MevShareEventListener;
+import net.flashbots.models.bundle.BundleItemType;
+import net.flashbots.models.bundle.BundleParams;
+import net.flashbots.models.bundle.PrivateTxOptions;
+import net.flashbots.models.bundle.PrivateTxParams;
+import net.flashbots.models.bundle.SendBundleResponse;
+import net.flashbots.models.bundle.SimBundleOptions;
+import net.flashbots.models.bundle.SimBundleResponse;
+import net.flashbots.models.common.JsonRpc20Request;
+import net.flashbots.models.common.Network;
+import net.flashbots.models.event.EventHistoryEntry;
+import net.flashbots.models.event.EventHistoryInfo;
+import net.flashbots.models.event.EventHistoryParams;
+import net.flashbots.models.event.MevShareEvent;
+import net.flashbots.provider.HttpProvider;
+import okhttp3.HttpUrl;
+import okhttp3.Request;
+import okhttp3.sse.EventSource;
+import org.slf4j.Logger;
+import org.web3j.crypto.Credentials;
+import org.web3j.protocol.Web3j;
+import org.web3j.protocol.core.methods.response.EthTransaction;
+
+/**
+ * The type MevShareClient.
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class MevShareClient implements MevShareApi {
+
+ private static final Logger LOGGER = getLogger(MevShareClient.class);
+
+ private static final ObjectMapper objectMapper = new ObjectMapper()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+ .setSerializationInclusion(JsonInclude.Include.NON_NULL);
+
+ private final Credentials authSigner;
+
+ private final HttpProvider provider;
+
+ private final Network network;
+
+ private final Web3j web3j;
+
+ /**
+ * Instantiates a new Mev share client.
+ *
+ * @param network the network
+ * @param authSigner the auth signer
+ * @param web3j the web3j client
+ */
+ public MevShareClient(Network network, Credentials authSigner, Web3j web3j) {
+ this.network = network;
+ this.provider = new HttpProvider(objectMapper);
+ this.authSigner = authSigner;
+ this.web3j = web3j;
+ }
+
+ @Override
+ public CompletableFuture getEventHistoryInfo() {
+ Request request = new Request.Builder()
+ .url(network.streamUrl() + "/api/v1/history/info")
+ .get()
+ .build();
+ return provider.send(request, objectMapper.constructType(EventHistoryInfo.class));
+ }
+
+ @Override
+ public CompletableFuture> getEventHistory(EventHistoryParams params) {
+ HttpUrl.Builder urlBuilder = Objects.requireNonNull(HttpUrl.parse(network.streamUrl() + "/api/v1/history"))
+ .newBuilder();
+ if (params != null) {
+ if (params.getBlockStart() != null) {
+ urlBuilder.addQueryParameter(
+ "blockStart", params.getBlockStart().toString());
+ }
+ if (params.getBlockEnd() != null) {
+ urlBuilder.addQueryParameter("blockEnd", params.getBlockEnd().toString());
+ }
+ if (params.getTimestampStart() != null) {
+ urlBuilder.addQueryParameter(
+ "timestampStart", params.getTimestampStart().toString());
+ }
+ if (params.getTimestampEnd() != null) {
+ urlBuilder.addQueryParameter(
+ "timestampEnd", params.getTimestampEnd().toString());
+ }
+ if (params.getLimit() != null) {
+ urlBuilder.addQueryParameter("limit", params.getLimit().toString());
+ }
+ if (params.getOffset() != null) {
+ urlBuilder.addQueryParameter("offset", params.getOffset().toString());
+ }
+ }
+ Request request =
+ new Request.Builder().url(urlBuilder.build().url()).get().build();
+ return provider.send(
+ request, objectMapper.getTypeFactory().constructCollectionType(List.class, EventHistoryEntry.class));
+ }
+
+ @Override
+ public Disposable subscribe(Consumer consumer) {
+ return Flowable.create(
+ subscriber -> {
+ Request request = new Request.Builder()
+ .url(network.streamUrl())
+ .get()
+ .build();
+ MevShareEventListener eventListener = new MevShareEventListener(subscriber, objectMapper);
+ final EventSource eventSource =
+ provider.eventSourceFactory().newEventSource(request, eventListener);
+ subscriber.setCancellable(eventSource::cancel);
+ },
+ BackpressureStrategy.MISSING)
+ .subscribe(consumer::accept);
+ }
+
+ @Override
+ public Disposable subscribeTx(Consumer consumer) {
+ return this.subscribe(mevShareEvent -> {
+ if (mevShareEvent.getTxs() == null || mevShareEvent.getTxs().size() == 1) {
+ consumer.accept(mevShareEvent);
+ }
+ });
+ }
+
+ @Override
+ public Disposable subscribeBundle(Consumer consumer) {
+ return this.subscribe(mevShareEvent -> {
+ if (mevShareEvent.getTxs() != null && mevShareEvent.getTxs().size() > 1) {
+ consumer.accept(mevShareEvent);
+ }
+ });
+ }
+
+ @Override
+ public CompletableFuture sendBundle(BundleParams params) {
+ JsonRpc20Request request = provider.createJsonRpc20Request("mev_sendBundle", List.of(params));
+ return provider.send(
+ network.rpcUrl(), request, authSigner, objectMapper.constructType(SendBundleResponse.class));
+ }
+
+ @Override
+ public CompletableFuture simBundle(BundleParams params, SimBundleOptions options) {
+ var realOptions = options == null ? new SimBundleOptions() : options;
+ JsonRpc20Request request = provider.createJsonRpc20Request("mev_simBundle", List.of(params, realOptions));
+ return provider.send(
+ network.rpcUrl(), request, authSigner, objectMapper.constructType(SimBundleResponse.class));
+ }
+
+ @Override
+ public CompletableFuture simulateBundle(
+ final BundleParams params, final SimBundleOptions options) {
+ if (!(params.getBody().get(0) instanceof BundleItemType.HashItem firstTx)) {
+ return this.simBundle(params, options);
+ }
+ return this.createSimulateBundle(firstTx, params, options);
+ }
+
+ @Override
+ public CompletableFuture sendPrivateTransaction(String signedRawTx, PrivateTxOptions options) {
+ var tx = PrivateTxParams.from(signedRawTx, options);
+ JsonRpc20Request request = provider.createJsonRpc20Request("eth_sendPrivateTransaction", List.of(tx));
+ return provider.send(network.rpcUrl(), request, authSigner, objectMapper.constructType(String.class));
+ }
+
+ private CompletableFuture createSimulateBundle(
+ final BundleItemType.HashItem firstTx, final BundleParams params, final SimBundleOptions options) {
+ return getTransaction(firstTx.getHash()).thenComposeAsync(tx -> {
+ if (tx.getTransaction().isEmpty()) {
+ throw new MevShareApiException("Target transaction did not appear on chain");
+ }
+ var simBlock = options != null
+ ? options.getParentBlock()
+ : tx.getTransaction().get().getBlockNumber().subtract(BigInteger.ONE);
+
+ var body = new ArrayList<>(params.getBody());
+ body.set(
+ 0,
+ new BundleItemType.TxItem()
+ .setTx(tx.getTransaction().get().getInput())
+ .setCanRevert(false));
+ var paramsWithSignedTx = params.clone().setBody(body);
+
+ var newOptions = options == null ? new SimBundleOptions() : options.clone();
+ newOptions.setParentBlock(simBlock);
+
+ return this.simBundle(paramsWithSignedTx, newOptions);
+ });
+ }
+
+ private CompletableFuture getTransaction(final String hash) {
+ return CompletableFuture.supplyAsync(() -> {
+ Disposable subscribe = null;
+ try {
+ // try to get tx first
+ EthTransaction res = web3j.ethGetTransactionByHash(hash).send();
+ if (res.getTransaction().isPresent()) {
+ return res;
+ }
+
+ final CompletableFuture txFuture = new CompletableFuture<>();
+ subscribe = web3j.blockFlowable(false).subscribe(block -> {
+ EthTransaction hashTx = web3j.ethGetTransactionByHash(hash).send();
+ if (hashTx.getTransaction().isPresent()) {
+ txFuture.complete(hashTx);
+ }
+ });
+ return txFuture.get(5, TimeUnit.MINUTES);
+ } catch (InterruptedException | ExecutionException | TimeoutException | IOException e) {
+ LOGGER.error("Failed to get transaction by hash", e);
+ if (e instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ throw new MevShareApiException("Failed to get transaction by hash", e);
+ } finally {
+ if (subscribe != null) {
+ subscribe.dispose();
+ }
+ }
+ });
+ }
+}
diff --git a/src/main/java/net/flashbots/common/MevShareApiException.java b/src/main/java/net/flashbots/common/MevShareApiException.java
new file mode 100644
index 0000000..6bc6cb9
--- /dev/null
+++ b/src/main/java/net/flashbots/common/MevShareApiException.java
@@ -0,0 +1,73 @@
+package net.flashbots.common;
+
+import net.flashbots.models.common.JsonRpc20Response;
+
+/**
+ * The type MevShareApiException
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class MevShareApiException extends RuntimeException {
+
+ /**
+ * The jsonRpcError
+ */
+ private JsonRpc20Response.JsonRpcError jsonRpcError;
+
+ /**
+ * Instantiates a new MevShareApiException.
+ *
+ * @param jsonRpcError the jsonRpcError
+ */
+ public MevShareApiException(JsonRpc20Response.JsonRpcError jsonRpcError) {
+ super();
+ this.jsonRpcError = jsonRpcError;
+ }
+
+ /**
+ * Instantiates a new MevShareApiException.
+ *
+ * @param cause the cause
+ */
+ public MevShareApiException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Instantiates a new MevShareApiException.
+ *
+ * @param message the message
+ * @param throwable the throwable
+ */
+ public MevShareApiException(String message, Throwable throwable) {
+ super(message, throwable);
+ }
+
+ /**
+ * Instantiates a new MevShareApiException.
+ *
+ * @param message the message
+ */
+ public MevShareApiException(String message) {
+ super(message);
+ }
+
+ /**
+ * Gets jsonRpcError.
+ *
+ * @return the jsonRpcError
+ */
+ public JsonRpc20Response.JsonRpcError getError() {
+ return jsonRpcError;
+ }
+
+ /**
+ * Sets jsonRpcError.
+ *
+ * @param jsonRpcError the jsonRpcError
+ */
+ public void setError(JsonRpc20Response.JsonRpcError jsonRpcError) {
+ this.jsonRpcError = jsonRpcError;
+ }
+}
diff --git a/src/main/java/net/flashbots/common/MevShareEventListener.java b/src/main/java/net/flashbots/common/MevShareEventListener.java
new file mode 100644
index 0000000..6daf79b
--- /dev/null
+++ b/src/main/java/net/flashbots/common/MevShareEventListener.java
@@ -0,0 +1,78 @@
+package net.flashbots.common;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.reactivex.FlowableEmitter;
+import net.flashbots.models.event.MevShareEvent;
+import okhttp3.Response;
+import okhttp3.sse.EventSource;
+import okhttp3.sse.EventSourceListener;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+
+/**
+ * The type MevShareEventListener.
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class MevShareEventListener extends EventSourceListener {
+
+ private static final Logger LOGGER = getLogger(MevShareEventListener.class);
+
+ private final FlowableEmitter emitter;
+
+ private final ObjectMapper objectMapper;
+
+ /**
+ * Instantiates a new MevShareEventListener.
+ *
+ * @param emitter the events emitter
+ * @param objectMapper the object mapper
+ */
+ public MevShareEventListener(FlowableEmitter emitter, ObjectMapper objectMapper) {
+ this.emitter = emitter;
+ this.objectMapper = objectMapper;
+ }
+
+ @Override
+ public void onEvent(EventSource eventSource, String id, String type, String data) {
+ LOGGER.trace("EventSource received event: id={}, type={}, data={}", id, type, data);
+ if (StringUtils.isEmpty(data) || ":ping".equals(data)) {
+ return;
+ }
+
+ try {
+ MevShareEvent mevShareEvent = objectMapper.readValue(data, MevShareEvent.class);
+ if (!this.emitter.isCancelled()) {
+ this.emitter.onNext(mevShareEvent);
+ }
+ } catch (JsonProcessingException e) {
+ LOGGER.error("JsonRpcError parsing response", e);
+ MevShareApiException mse = new MevShareApiException(e);
+ if (this.emitter != null && !this.emitter.isCancelled()) {
+ this.emitter.onError(mse);
+ }
+ throw mse;
+ }
+ }
+
+ @Override
+ public void onClosed(EventSource eventSource) {
+ LOGGER.trace("EventSource closed");
+ this.emitter.onComplete();
+ }
+
+ @Override
+ public void onFailure(EventSource eventSource, Throwable t, Response response) {
+ LOGGER.error("EventSource failed", t);
+ this.emitter.onComplete();
+ }
+
+ @Override
+ public void onOpen(EventSource eventSource, Response response) {
+ LOGGER.trace("EventSource opened");
+ }
+}
diff --git a/src/main/java/net/flashbots/models/bundle/BundleItemType.java b/src/main/java/net/flashbots/models/bundle/BundleItemType.java
new file mode 100644
index 0000000..4ca7f9c
--- /dev/null
+++ b/src/main/java/net/flashbots/models/bundle/BundleItemType.java
@@ -0,0 +1,193 @@
+package net.flashbots.models.bundle;
+
+import java.util.Objects;
+
+/**
+ * The type BundleItemType.
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public abstract class BundleItemType {
+
+ /**
+ * The type HashItem.
+ */
+ public static class HashItem extends BundleItemType {
+ private String hash;
+
+ /**
+ * Instantiates a new Hash item.
+ */
+ public HashItem() {
+ super();
+ }
+
+ /**
+ * Gets hash.
+ *
+ * @return the hash
+ */
+ public String getHash() {
+ return hash;
+ }
+
+ /**
+ * Sets hash.
+ *
+ * @param hash the hash
+ * @return the hash
+ */
+ public HashItem setHash(String hash) {
+ this.hash = hash;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof HashItem hashItem)) return false;
+ return Objects.equals(hash, hashItem.hash);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(hash);
+ }
+
+ @Override
+ public String toString() {
+ return "HashItem{" + "hash='" + hash + '\'' + '}';
+ }
+ }
+
+ /**
+ * The type HashItem.
+ */
+ public static class TxItem extends BundleItemType {
+
+ private String tx;
+
+ private boolean canRevert;
+
+ /**
+ * Instantiates a new Tx item.
+ */
+ public TxItem() {
+ super();
+ }
+
+ /**
+ * Gets tx.
+ *
+ * @return the tx
+ */
+ public String getTx() {
+ return tx;
+ }
+
+ /**
+ * Sets tx.
+ *
+ * @param tx the tx
+ * @return the tx
+ */
+ public TxItem setTx(String tx) {
+ this.tx = tx;
+ return this;
+ }
+
+ /**
+ * Is can revert boolean.
+ *
+ * @return the boolean
+ */
+ public boolean isCanRevert() {
+ return canRevert;
+ }
+
+ /**
+ * Sets can revert.
+ *
+ * @param canRevert the can revert
+ * @return the can revert
+ */
+ public TxItem setCanRevert(boolean canRevert) {
+ this.canRevert = canRevert;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof TxItem txItem)) return false;
+ return canRevert == txItem.canRevert && Objects.equals(tx, txItem.tx);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(tx, canRevert);
+ }
+
+ @Override
+ public String toString() {
+ return "TxItem{" + "tx='" + tx + '\'' + ", canRevert=" + canRevert + '}';
+ }
+ }
+
+ /**
+ * The type Bundle item.
+ */
+ public static class BundleItem extends BundleItemType {
+ private BundleParams bundle;
+
+ /**
+ * Instantiates a new Bundle item.
+ */
+ public BundleItem() {
+ super();
+ }
+
+ /**
+ * Gets bundle.
+ *
+ * @return the bundle
+ */
+ public BundleParams getBundle() {
+ return bundle;
+ }
+
+ /**
+ * Sets bundle.
+ *
+ * @param bundle the bundle
+ * @return the bundle
+ */
+ public BundleItem setBundle(BundleParams bundle) {
+ this.bundle = bundle;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof BundleItem that)) return false;
+ return Objects.equals(bundle, that.bundle);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(bundle);
+ }
+
+ @Override
+ public String toString() {
+ return "BundleItem{" + "bundle=" + bundle + '}';
+ }
+ }
+
+ /**
+ * Instantiates a new BundleItemType.
+ */
+ public BundleItemType() {}
+}
diff --git a/src/main/java/net/flashbots/models/bundle/BundleParams.java b/src/main/java/net/flashbots/models/bundle/BundleParams.java
new file mode 100644
index 0000000..000e797
--- /dev/null
+++ b/src/main/java/net/flashbots/models/bundle/BundleParams.java
@@ -0,0 +1,190 @@
+package net.flashbots.models.bundle;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * The type Bundle params.
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class BundleParams {
+
+ private String version = "v0.1";
+
+ private Inclusion inclusion;
+
+ private List body;
+
+ private Validity validity;
+
+ private BundlePrivacy privacy;
+
+ private Metadata metadata;
+
+ /**
+ * Instantiates a new Bundle params.
+ */
+ public BundleParams() {}
+
+ /**
+ * Gets version.
+ *
+ * @return the version
+ */
+ public String getVersion() {
+ return version;
+ }
+
+ /**
+ * Sets version.
+ *
+ * @param version the version
+ * @return the version
+ */
+ public BundleParams setVersion(String version) {
+ this.version = version;
+ return this;
+ }
+
+ /**
+ * Gets inclusion.
+ *
+ * @return the inclusion
+ */
+ public Inclusion getInclusion() {
+ return inclusion;
+ }
+
+ /**
+ * Sets inclusion.
+ *
+ * @param inclusion the inclusion
+ * @return the inclusion
+ */
+ public BundleParams setInclusion(Inclusion inclusion) {
+ this.inclusion = inclusion;
+ return this;
+ }
+
+ /**
+ * Gets body.
+ *
+ * @return the body
+ */
+ public List getBody() {
+ return body;
+ }
+
+ /**
+ * Sets body.
+ *
+ * @param body the body
+ * @return the body
+ */
+ public BundleParams setBody(List body) {
+ this.body = body;
+ return this;
+ }
+
+ /**
+ * Gets validity.
+ *
+ * @return the validity
+ */
+ public Validity getValidity() {
+ return validity;
+ }
+
+ /**
+ * Sets validity.
+ *
+ * @param validity the validity
+ * @return the validity
+ */
+ public BundleParams setValidity(Validity validity) {
+ this.validity = validity;
+ return this;
+ }
+
+ /**
+ * Gets privacy.
+ *
+ * @return the privacy
+ */
+ public BundlePrivacy getPrivacy() {
+ return privacy;
+ }
+
+ /**
+ * Sets privacy.
+ *
+ * @param privacy the privacy
+ * @return the privacy
+ */
+ public BundleParams setPrivacy(BundlePrivacy privacy) {
+ this.privacy = privacy;
+ return this;
+ }
+
+ /**
+ * Gets metadata.
+ *
+ * @return the metadata
+ */
+ public Metadata getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * Sets metadata.
+ *
+ * @param metadata the metadata
+ * @return the metadata
+ */
+ public BundleParams setMetadata(Metadata metadata) {
+ this.metadata = metadata;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof BundleParams that)) return false;
+ return Objects.equals(version, that.version)
+ && Objects.equals(inclusion, that.inclusion)
+ && Objects.equals(body, that.body)
+ && Objects.equals(validity, that.validity)
+ && Objects.equals(privacy, that.privacy)
+ && Objects.equals(metadata, that.metadata);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(version, inclusion, body, validity, privacy, metadata);
+ }
+
+ @Override
+ public String toString() {
+ return "BundleParams{" + "version='"
+ + version + '\'' + ", inclusion="
+ + inclusion + ", body="
+ + body + ", validity="
+ + validity + ", privacy="
+ + privacy + ", metadata="
+ + metadata + '}';
+ }
+
+ @Override
+ public BundleParams clone() {
+ BundleParams clone = new BundleParams();
+ clone.setVersion(version);
+ clone.setInclusion(inclusion);
+ clone.setBody(body);
+ clone.setValidity(validity);
+ clone.setPrivacy(privacy);
+ clone.setMetadata(metadata);
+ return clone;
+ }
+}
diff --git a/src/main/java/net/flashbots/models/bundle/BundlePrivacy.java b/src/main/java/net/flashbots/models/bundle/BundlePrivacy.java
new file mode 100644
index 0000000..abba5ee
--- /dev/null
+++ b/src/main/java/net/flashbots/models/bundle/BundlePrivacy.java
@@ -0,0 +1,80 @@
+package net.flashbots.models.bundle;
+
+import java.util.List;
+import java.util.Objects;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import net.flashbots.provider.json.HintPreferencesSerializer;
+
+/**
+ * The type Bundle privacy.
+ */
+public class BundlePrivacy {
+
+ private List builders;
+
+ @JsonSerialize(using = HintPreferencesSerializer.class)
+ private HintPreferences hints;
+
+ /**
+ * Instantiates a new Bundle privacy.
+ */
+ public BundlePrivacy() {}
+
+ /**
+ * Gets builders.
+ *
+ * @return the builders
+ */
+ public List getBuilders() {
+ return builders;
+ }
+
+ /**
+ * Sets builders.
+ *
+ * @param builders the builders
+ * @return the builders
+ */
+ public BundlePrivacy setBuilders(List builders) {
+ this.builders = builders;
+ return this;
+ }
+
+ /**
+ * Gets hints.
+ *
+ * @return the hints
+ */
+ public HintPreferences getHints() {
+ return hints;
+ }
+
+ /**
+ * Sets hints.
+ *
+ * @param hints the hints
+ * @return the hints
+ */
+ public BundlePrivacy setHints(HintPreferences hints) {
+ this.hints = hints;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof BundlePrivacy that)) return false;
+ return Objects.equals(builders, that.builders) && Objects.equals(hints, that.hints);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(builders, hints);
+ }
+
+ @Override
+ public String toString() {
+ return "BundlePrivacy{" + "builders=" + builders + ", hints=" + hints + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/models/bundle/HintPreferences.java b/src/main/java/net/flashbots/models/bundle/HintPreferences.java
new file mode 100644
index 0000000..3765d0f
--- /dev/null
+++ b/src/main/java/net/flashbots/models/bundle/HintPreferences.java
@@ -0,0 +1,149 @@
+package net.flashbots.models.bundle;
+
+import java.util.Objects;
+
+/**
+ * The type Hint preferences.
+ */
+public class HintPreferences {
+ private boolean calldata;
+
+ private boolean contractAddress;
+
+ private boolean functionSelector;
+
+ private boolean logs;
+
+ private boolean txHash;
+
+ /**
+ * Instantiates a new Hint preferences.
+ */
+ public HintPreferences() {}
+
+ /**
+ * Is calldata boolean.
+ *
+ * @return the boolean
+ */
+ public boolean isCalldata() {
+ return calldata;
+ }
+
+ /**
+ * Sets calldata.
+ *
+ * @param calldata the calldata
+ * @return the calldata
+ */
+ public HintPreferences setCalldata(boolean calldata) {
+ this.calldata = calldata;
+ return this;
+ }
+
+ /**
+ * Is contract address boolean.
+ *
+ * @return the boolean
+ */
+ public boolean isContractAddress() {
+ return contractAddress;
+ }
+
+ /**
+ * Sets contract address.
+ *
+ * @param contractAddress the contract address
+ * @return the contract address
+ */
+ public HintPreferences setContractAddress(boolean contractAddress) {
+ this.contractAddress = contractAddress;
+ return this;
+ }
+
+ /**
+ * Is function selector boolean.
+ *
+ * @return the boolean
+ */
+ public boolean isFunctionSelector() {
+ return functionSelector;
+ }
+
+ /**
+ * Sets function selector.
+ *
+ * @param functionSelector the function selector
+ * @return the function selector
+ */
+ public HintPreferences setFunctionSelector(boolean functionSelector) {
+ this.functionSelector = functionSelector;
+ return this;
+ }
+
+ /**
+ * Is logs boolean.
+ *
+ * @return the boolean
+ */
+ public boolean isLogs() {
+ return logs;
+ }
+
+ /**
+ * Sets logs.
+ *
+ * @param logs the logs
+ * @return the logs
+ */
+ public HintPreferences setLogs(boolean logs) {
+ this.logs = logs;
+ return this;
+ }
+
+ /**
+ * Is tx hash boolean.
+ *
+ * @return the boolean
+ */
+ public boolean isTxHash() {
+ return txHash;
+ }
+
+ /**
+ * Sets tx hash.
+ *
+ * @param txHash the tx hash
+ * @return the tx hash
+ */
+ public HintPreferences setTxHash(boolean txHash) {
+ this.txHash = txHash;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof HintPreferences that)) return false;
+ return calldata == that.calldata
+ && contractAddress == that.contractAddress
+ && functionSelector == that.functionSelector
+ && logs == that.logs
+ && txHash == that.txHash;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(calldata, contractAddress, functionSelector, logs, txHash);
+ }
+
+ @Override
+ public String toString() {
+ return "HintPreferences{" + "calldata="
+ + calldata + ", contractAddress="
+ + contractAddress + ", functionSelector="
+ + functionSelector + ", logs="
+ + logs + ", txHash="
+ + txHash + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/models/bundle/Inclusion.java b/src/main/java/net/flashbots/models/bundle/Inclusion.java
new file mode 100644
index 0000000..f542fc0
--- /dev/null
+++ b/src/main/java/net/flashbots/models/bundle/Inclusion.java
@@ -0,0 +1,81 @@
+package net.flashbots.models.bundle;
+
+import java.math.BigInteger;
+import java.util.Objects;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import net.flashbots.provider.json.BigIntToHexStringSerializer;
+
+/**
+ * The type Inclusion.
+ */
+public class Inclusion {
+
+ @JsonSerialize(using = BigIntToHexStringSerializer.class)
+ private BigInteger block;
+
+ @JsonSerialize(using = BigIntToHexStringSerializer.class)
+ private BigInteger maxBlock;
+
+ /**
+ * Instantiates a new Inclusion.
+ */
+ public Inclusion() {}
+
+ /**
+ * Gets block.
+ *
+ * @return the block
+ */
+ public BigInteger getBlock() {
+ return block;
+ }
+
+ /**
+ * Sets block.
+ *
+ * @param block the block
+ * @return the block
+ */
+ public Inclusion setBlock(BigInteger block) {
+ this.block = block;
+ return this;
+ }
+
+ /**
+ * Gets max block.
+ *
+ * @return the max block
+ */
+ public BigInteger getMaxBlock() {
+ return maxBlock;
+ }
+
+ /**
+ * Sets max block.
+ *
+ * @param maxBlock the max block
+ * @return the max block
+ */
+ public Inclusion setMaxBlock(BigInteger maxBlock) {
+ this.maxBlock = maxBlock;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Inclusion inclusion)) return false;
+ return Objects.equals(block, inclusion.block) && Objects.equals(maxBlock, inclusion.maxBlock);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(block, maxBlock);
+ }
+
+ @Override
+ public String toString() {
+ return "Inclusion{" + "block=" + block + ", maxBlock=" + maxBlock + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/models/bundle/Metadata.java b/src/main/java/net/flashbots/models/bundle/Metadata.java
new file mode 100644
index 0000000..b913392
--- /dev/null
+++ b/src/main/java/net/flashbots/models/bundle/Metadata.java
@@ -0,0 +1,53 @@
+package net.flashbots.models.bundle;
+
+import java.util.Objects;
+
+/**
+ * The type Metadata.
+ */
+public class Metadata {
+
+ private String originId;
+
+ /**
+ * Instantiates a new Metadata.
+ */
+ public Metadata() {}
+
+ /**
+ * Gets origin id.
+ *
+ * @return the origin id
+ */
+ public String getOriginId() {
+ return originId;
+ }
+
+ /**
+ * Sets origin id.
+ *
+ * @param originId the origin id
+ * @return the origin id
+ */
+ public Metadata setOriginId(String originId) {
+ this.originId = originId;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Metadata metadata)) return false;
+ return Objects.equals(originId, metadata.originId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(originId);
+ }
+
+ @Override
+ public String toString() {
+ return "Metadata{" + "originId='" + originId + '\'' + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/models/bundle/PrivateTxOptions.java b/src/main/java/net/flashbots/models/bundle/PrivateTxOptions.java
new file mode 100644
index 0000000..11f44ee
--- /dev/null
+++ b/src/main/java/net/flashbots/models/bundle/PrivateTxOptions.java
@@ -0,0 +1,111 @@
+package net.flashbots.models.bundle;
+
+import java.math.BigInteger;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * The type PrivateTxOptions
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class PrivateTxOptions {
+
+ private HintPreferences hints;
+
+ private BigInteger maxBlockNumber;
+
+ private List builders;
+
+ /**
+ * Instantiates a new private transaction options.
+ */
+ public PrivateTxOptions() {}
+
+ /**
+ * Gets hints
+ *
+ * @return the hints
+ */
+ public HintPreferences getHints() {
+ return hints;
+ }
+
+ /**
+ * Sets hints
+ *
+ * @param hints the hints
+ * @return this private transaction options
+ */
+ public PrivateTxOptions setHints(HintPreferences hints) {
+ this.hints = hints;
+ return this;
+ }
+
+ /**
+ * Gets max block number
+ *
+ * @return the max block number
+ */
+ public BigInteger getMaxBlockNumber() {
+ return maxBlockNumber;
+ }
+
+ /**
+ * Sets max block number
+ *
+ * @param maxBlockNumber the max block number
+ * @return this private transaction options
+ */
+ public PrivateTxOptions setMaxBlockNumber(BigInteger maxBlockNumber) {
+ this.maxBlockNumber = maxBlockNumber;
+ return this;
+ }
+
+ /**
+ * Gets builders
+ *
+ * @return the builders
+ */
+ public List getBuilders() {
+ return builders;
+ }
+
+ /**
+ * Sets builders
+ *
+ * @param builders the builders
+ * @return this private transaction options
+ */
+ public PrivateTxOptions setBuilders(List builders) {
+ this.builders = builders;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof PrivateTxOptions that)) {
+ return false;
+ }
+ return Objects.equals(hints, that.hints)
+ && Objects.equals(maxBlockNumber, that.maxBlockNumber)
+ && Objects.equals(builders, that.builders);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(hints, maxBlockNumber, builders);
+ }
+
+ @Override
+ public String toString() {
+ return "PrivateTxOptions{" + "hints="
+ + hints + ", maxBlockNumber="
+ + maxBlockNumber + ", builders="
+ + builders + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/models/bundle/PrivateTxParams.java b/src/main/java/net/flashbots/models/bundle/PrivateTxParams.java
new file mode 100644
index 0000000..5451d23
--- /dev/null
+++ b/src/main/java/net/flashbots/models/bundle/PrivateTxParams.java
@@ -0,0 +1,239 @@
+package net.flashbots.models.bundle;
+
+import java.math.BigInteger;
+import java.util.List;
+import java.util.Objects;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import net.flashbots.provider.json.BigIntToHexStringSerializer;
+
+/**
+ * The type PrivateTxParams
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class PrivateTxParams {
+
+ private String tx;
+
+ @JsonSerialize(using = BigIntToHexStringSerializer.class)
+ private BigInteger maxBlockNumber;
+
+ private Preferences preferences;
+
+ /**
+ * Instantiates a new private transaction params.
+ */
+ public PrivateTxParams() {}
+
+ /**
+ * Gets transaction
+ *
+ * @return the transaction
+ */
+ public String getTx() {
+ return tx;
+ }
+
+ /**
+ * Sets transaction
+ *
+ * @param tx signed transaction hex string
+ * @return this private transaction params
+ */
+ public PrivateTxParams setTx(String tx) {
+ this.tx = tx;
+ return this;
+ }
+
+ /**
+ * Gets max block number
+ *
+ * @return the max block number
+ */
+ public BigInteger getMaxBlockNumber() {
+ return maxBlockNumber;
+ }
+
+ /**
+ * Sets the max block number
+ *
+ * @param maxBlockNumber the max block number
+ * @return this private transaction params
+ */
+ public PrivateTxParams setMaxBlockNumber(BigInteger maxBlockNumber) {
+ this.maxBlockNumber = maxBlockNumber;
+ return this;
+ }
+
+ /**
+ * Gets preferences
+ *
+ * @return the preferences
+ */
+ public Preferences getPreferences() {
+ return preferences;
+ }
+
+ /**
+ * Sets the preferences
+ *
+ * @param preferences the preferences
+ * @return this private transaction params
+ */
+ public PrivateTxParams setPreferences(Preferences preferences) {
+ this.preferences = preferences;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof PrivateTxParams that)) {
+ return false;
+ }
+ return Objects.equals(tx, that.tx)
+ && Objects.equals(maxBlockNumber, that.maxBlockNumber)
+ && Objects.equals(preferences, that.preferences);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(tx, maxBlockNumber, preferences);
+ }
+
+ @Override
+ public String toString() {
+ return "PrivateTxParams{" + "tx='"
+ + tx + '\'' + ", maxBlockNumber="
+ + maxBlockNumber + ", preferences="
+ + preferences + '}';
+ }
+
+ /**
+ * Create private tx params from signed tx and options.
+ *
+ * @param tx signed tx
+ * @param options private tx options
+ * @return the private tx params
+ */
+ public static PrivateTxParams from(String tx, PrivateTxOptions options) {
+ var params = new PrivateTxParams();
+ params.setTx(tx);
+
+ Preferences preferences = new Preferences();
+ preferences.setFast(true);
+
+ if (options != null) {
+ params.setMaxBlockNumber(options.getMaxBlockNumber());
+
+ preferences.setBuilders(options.getBuilders());
+ preferences.setPrivacy(new BundlePrivacy().setHints(options.getHints()));
+ }
+ params.setPreferences(preferences);
+ return params;
+ }
+
+ /**
+ * the type preferences
+ */
+ public static class Preferences {
+
+ private boolean fast;
+
+ private BundlePrivacy privacy;
+
+ private List builders;
+
+ /**
+ * Instantiates a new preferences
+ */
+ public Preferences() {}
+
+ /**
+ * Gets fast flag.
+ *
+ * @return the fast flag
+ */
+ public boolean getFast() {
+ return fast;
+ }
+
+ /**
+ * Sets the fast flag
+ *
+ * @param fast the fast flag
+ * @return this preferences
+ */
+ public Preferences setFast(boolean fast) {
+ this.fast = fast;
+ return this;
+ }
+
+ /**
+ * Gets bundle privacy
+ *
+ * @return the bundle privacy
+ */
+ public BundlePrivacy getPrivacy() {
+ return privacy;
+ }
+
+ /**
+ * Sets the bundle privacy
+ *
+ * @param privacy the bundle privacy
+ * @return this preferences
+ */
+ public Preferences setPrivacy(BundlePrivacy privacy) {
+ this.privacy = privacy;
+ return this;
+ }
+
+ /**
+ * Gets the builders
+ *
+ * @return the builders
+ */
+ public List getBuilders() {
+ return builders;
+ }
+
+ /**
+ * Sets the builders
+ *
+ * @param builders the buidlers
+ * @return this preferences
+ */
+ public Preferences setBuilders(List builders) {
+ this.builders = builders;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof Preferences that)) {
+ return false;
+ }
+ return fast == that.fast
+ && Objects.equals(privacy, that.privacy)
+ && Objects.equals(builders, that.builders);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(fast, privacy, builders);
+ }
+
+ @Override
+ public String toString() {
+ return "Preferences{" + "fast=" + fast + ", privacy=" + privacy + ", builders=" + builders + '}';
+ }
+ }
+}
diff --git a/src/main/java/net/flashbots/models/bundle/Refund.java b/src/main/java/net/flashbots/models/bundle/Refund.java
new file mode 100644
index 0000000..780b202
--- /dev/null
+++ b/src/main/java/net/flashbots/models/bundle/Refund.java
@@ -0,0 +1,75 @@
+package net.flashbots.models.bundle;
+
+import java.util.Objects;
+
+/**
+ * The type Refund.
+ */
+public class Refund {
+
+ private Integer bodyIdx;
+
+ private Integer percent;
+
+ /**
+ * Instantiates a new Refund.
+ */
+ public Refund() {}
+
+ /**
+ * Gets body idx.
+ *
+ * @return the body idx
+ */
+ public Integer getBodyIdx() {
+ return bodyIdx;
+ }
+
+ /**
+ * Sets body idx.
+ *
+ * @param bodyIdx the body idx
+ * @return the body idx
+ */
+ public Refund setBodyIdx(Integer bodyIdx) {
+ this.bodyIdx = bodyIdx;
+ return this;
+ }
+
+ /**
+ * Gets percent.
+ *
+ * @return the percent
+ */
+ public Integer getPercent() {
+ return percent;
+ }
+
+ /**
+ * Sets percent.
+ *
+ * @param percent the percent
+ * @return the percent
+ */
+ public Refund setPercent(Integer percent) {
+ this.percent = percent;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Refund that)) return false;
+ return Objects.equals(bodyIdx, that.bodyIdx) && Objects.equals(percent, that.percent);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(bodyIdx, percent);
+ }
+
+ @Override
+ public String toString() {
+ return "Refund{" + "bodyIdx=" + bodyIdx + ", percent=" + percent + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/models/bundle/RefundConfig.java b/src/main/java/net/flashbots/models/bundle/RefundConfig.java
new file mode 100644
index 0000000..873f157
--- /dev/null
+++ b/src/main/java/net/flashbots/models/bundle/RefundConfig.java
@@ -0,0 +1,75 @@
+package net.flashbots.models.bundle;
+
+import java.util.Objects;
+
+/**
+ * The type Refund config.
+ */
+public class RefundConfig {
+
+ private String address;
+
+ private Integer percent;
+
+ /**
+ * Instantiates a new Refund config.
+ */
+ public RefundConfig() {}
+
+ /**
+ * Gets address.
+ *
+ * @return the address
+ */
+ public String getAddress() {
+ return address;
+ }
+
+ /**
+ * Sets address.
+ *
+ * @param address the address
+ * @return the address
+ */
+ public RefundConfig setAddress(String address) {
+ this.address = address;
+ return this;
+ }
+
+ /**
+ * Gets percent.
+ *
+ * @return the percent
+ */
+ public Integer getPercent() {
+ return percent;
+ }
+
+ /**
+ * Sets percent.
+ *
+ * @param percent the percent
+ * @return the percent
+ */
+ public RefundConfig setPercent(Integer percent) {
+ this.percent = percent;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof RefundConfig refundConfig)) return false;
+ return Objects.equals(address, refundConfig.address) && Objects.equals(percent, refundConfig.percent);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(address, percent);
+ }
+
+ @Override
+ public String toString() {
+ return "RefundConfig{" + "address='" + address + '\'' + ", percent=" + percent + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/models/bundle/SendBundleResponse.java b/src/main/java/net/flashbots/models/bundle/SendBundleResponse.java
new file mode 100644
index 0000000..9a2f6bd
--- /dev/null
+++ b/src/main/java/net/flashbots/models/bundle/SendBundleResponse.java
@@ -0,0 +1,53 @@
+package net.flashbots.models.bundle;
+
+import java.util.Objects;
+
+/**
+ * The type Send bundle response.
+ */
+public class SendBundleResponse {
+
+ private String bundleHash;
+
+ /**
+ * Instantiates a new Send bundle response.
+ */
+ public SendBundleResponse() {}
+
+ /**
+ * Gets bundle hash.
+ *
+ * @return the bundle hash
+ */
+ public String getBundleHash() {
+ return bundleHash;
+ }
+
+ /**
+ * Sets bundle hash.
+ *
+ * @param bundleHash the bundle hash
+ * @return the bundle hash
+ */
+ public SendBundleResponse setBundleHash(String bundleHash) {
+ this.bundleHash = bundleHash;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SendBundleResponse that)) return false;
+ return Objects.equals(bundleHash, that.bundleHash);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(bundleHash);
+ }
+
+ @Override
+ public String toString() {
+ return "SendBundleResponse{" + "bundleHash='" + bundleHash + '\'' + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/models/bundle/SimBundleLogs.java b/src/main/java/net/flashbots/models/bundle/SimBundleLogs.java
new file mode 100644
index 0000000..98c7f56
--- /dev/null
+++ b/src/main/java/net/flashbots/models/bundle/SimBundleLogs.java
@@ -0,0 +1,93 @@
+package net.flashbots.models.bundle;
+
+import java.util.List;
+import java.util.Objects;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import org.web3j.protocol.core.methods.response.EthLog;
+
+/**
+ * The type SimBundleLogs
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class SimBundleLogs {
+
+ /**
+ * Logs inside transactions.
+ */
+ @JsonDeserialize(using = EthLog.LogResultDeserialiser.class)
+ private List> txLogs;
+
+ /**
+ * Logs for bundles inside bundle.
+ */
+ private SimBundleLogs bundleLogs;
+
+ /**
+ * Instantiates a new simulate bundle logs.
+ */
+ public SimBundleLogs() {}
+
+ /**
+ * Gets list of tx logs
+ *
+ * @return list of tx logs
+ */
+ public List> getTxLogs() {
+ return txLogs;
+ }
+
+ /**
+ * Sets the list of tx logs
+ *
+ * @param txLogs the list of tx logs
+ * @return this simulates bundle logs
+ */
+ public SimBundleLogs setTxLogs(List> txLogs) {
+ this.txLogs = txLogs;
+ return this;
+ }
+
+ /**
+ * Gets the simulates bundle logs
+ *
+ * @return the simulates bundle logs
+ */
+ public SimBundleLogs getBundleLogs() {
+ return bundleLogs;
+ }
+
+ /**
+ * Sets the simulates bundle logs
+ *
+ * @param bundleLogs the simulates bundle logs
+ * @return this simulates bundle logs
+ */
+ public SimBundleLogs setBundleLogs(SimBundleLogs bundleLogs) {
+ this.bundleLogs = bundleLogs;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SimBundleLogs that)) {
+ return false;
+ }
+ return Objects.equals(txLogs, that.txLogs) && Objects.equals(bundleLogs, that.bundleLogs);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(txLogs, bundleLogs);
+ }
+
+ @Override
+ public String toString() {
+ return "SimBundleLogs{" + "txLogs=" + txLogs + ", bundleLogs=" + bundleLogs + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/models/bundle/SimBundleOptions.java b/src/main/java/net/flashbots/models/bundle/SimBundleOptions.java
new file mode 100644
index 0000000..4411faa
--- /dev/null
+++ b/src/main/java/net/flashbots/models/bundle/SimBundleOptions.java
@@ -0,0 +1,249 @@
+package net.flashbots.models.bundle;
+
+import java.math.BigInteger;
+import java.util.Objects;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import net.flashbots.provider.json.BigIntToHexStringSerializer;
+
+/**
+ * The type SimBundleOptions
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class SimBundleOptions {
+
+ /**
+ * Block used for simulation state. Defaults to latest block.
+ */
+ @JsonSerialize(using = BigIntToHexStringSerializer.class)
+ private BigInteger parentBlock;
+
+ /**
+ * Block number used for simulation, defaults to parentBlock.number + 1.
+ */
+ @JsonSerialize(using = BigIntToHexStringSerializer.class)
+ private BigInteger blockNumber;
+
+ /**
+ * Coinbase used for simulation, defaults to parentBlock.coinbase.
+ */
+ private String coinbase;
+
+ /**
+ * Timestamp used for simulation, defaults to parentBlock.timestamp + 12.
+ */
+ @JsonSerialize(using = BigIntToHexStringSerializer.class)
+ private BigInteger timestamp;
+
+ /**
+ * Gas limit used for simulation, defaults to parentBlock.gasLimit.
+ */
+ @JsonSerialize(using = BigIntToHexStringSerializer.class)
+ private BigInteger gasLimit;
+
+ /**
+ * Base fee used for simulation, defaults to parentBlock.baseFeePerGas.
+ */
+ @JsonSerialize(using = BigIntToHexStringSerializer.class)
+ private BigInteger baseFee;
+
+ /**
+ * Timeout in seconds, defaults to 5.
+ */
+ private Integer timeout;
+
+ /**
+ * Instantiates a new simulate bundle options.
+ */
+ public SimBundleOptions() {}
+
+ /**
+ * Gets parent block number
+ *
+ * @return the parent block number
+ */
+ public BigInteger getParentBlock() {
+ return parentBlock;
+ }
+
+ /**
+ * Sets parent block number
+ *
+ * @param parentBlock the parent block number
+ * @return this simulates bundle options
+ */
+ public SimBundleOptions setParentBlock(BigInteger parentBlock) {
+ this.parentBlock = parentBlock;
+ return this;
+ }
+
+ /**
+ * Gets block number
+ *
+ * @return the block number
+ */
+ public BigInteger getBlockNumber() {
+ return blockNumber;
+ }
+
+ /**
+ * Sets block number
+ *
+ * @param blockNumber the block number
+ * @return this simulates bundle options
+ */
+ public SimBundleOptions setBlockNumber(BigInteger blockNumber) {
+ this.blockNumber = blockNumber;
+ return this;
+ }
+
+ /**
+ * Gets the coinbase
+ *
+ * @return the coinbase
+ */
+ public String getCoinbase() {
+ return coinbase;
+ }
+
+ /**
+ * Sets coinbase
+ *
+ * @param coinbase the coinbase
+ * @return this simulates bundle options
+ */
+ public SimBundleOptions setCoinbase(String coinbase) {
+ this.coinbase = coinbase;
+ return this;
+ }
+
+ /**
+ * Gets the timestamp
+ *
+ * @return the timestamp
+ */
+ public BigInteger getTimestamp() {
+ return timestamp;
+ }
+
+ /**
+ * Sets timestamp
+ *
+ * @param timestamp the timestamp
+ * @return this simulates bundle options
+ */
+ public SimBundleOptions setTimestamp(BigInteger timestamp) {
+ this.timestamp = timestamp;
+ return this;
+ }
+
+ /**
+ * Gets gas limit
+ *
+ * @return the gas limit
+ */
+ public BigInteger getGasLimit() {
+ return gasLimit;
+ }
+
+ /**
+ * Sets gas limit
+ *
+ * @param gasLimit the gas limit
+ * @return this simulates bundle options
+ */
+ public SimBundleOptions setGasLimit(BigInteger gasLimit) {
+ this.gasLimit = gasLimit;
+ return this;
+ }
+
+ /**
+ * Gets the base fee
+ *
+ * @return the base fee
+ */
+ public BigInteger getBaseFee() {
+ return baseFee;
+ }
+
+ /**
+ * Sets base fee
+ *
+ * @param baseFee the base fee
+ * @return this simulates bundle options
+ */
+ public SimBundleOptions setBaseFee(BigInteger baseFee) {
+ this.baseFee = baseFee;
+ return this;
+ }
+
+ /**
+ * Gets the timeout
+ *
+ * @return the timeout
+ */
+ public Integer getTimeout() {
+ return timeout;
+ }
+
+ /**
+ * Sets timeout
+ *
+ * @param timeout the timeout
+ * @return this simulates bundle options
+ */
+ public SimBundleOptions setTimeout(Integer timeout) {
+ this.timeout = timeout;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SimBundleOptions)) {
+ return false;
+ }
+ SimBundleOptions that = (SimBundleOptions) o;
+ return Objects.equals(parentBlock, that.parentBlock)
+ && Objects.equals(blockNumber, that.blockNumber)
+ && Objects.equals(coinbase, that.coinbase)
+ && Objects.equals(timestamp, that.timestamp)
+ && Objects.equals(gasLimit, that.gasLimit)
+ && Objects.equals(baseFee, that.baseFee)
+ && Objects.equals(timeout, that.timeout);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(parentBlock, blockNumber, coinbase, timestamp, gasLimit, baseFee, timeout);
+ }
+
+ @Override
+ public String toString() {
+ return "SimBundleoptions{" + "parentBlock="
+ + parentBlock + ", blockNumber="
+ + blockNumber + ", coinbase='"
+ + coinbase + '\'' + ", timestamp="
+ + timestamp + ", gasLimit="
+ + gasLimit + ", baseFee="
+ + baseFee + ", timeout="
+ + timeout + '}';
+ }
+
+ @Override
+ public SimBundleOptions clone() {
+ final SimBundleOptions clone = new SimBundleOptions();
+ clone.setParentBlock(this.parentBlock)
+ .setBlockNumber(this.blockNumber)
+ .setCoinbase(this.coinbase)
+ .setTimestamp(this.timestamp)
+ .setGasLimit(this.gasLimit)
+ .setBaseFee(this.baseFee)
+ .setTimeout(this.timeout);
+ return clone;
+ }
+}
diff --git a/src/main/java/net/flashbots/models/bundle/SimBundleResponse.java b/src/main/java/net/flashbots/models/bundle/SimBundleResponse.java
new file mode 100644
index 0000000..3db0a35
--- /dev/null
+++ b/src/main/java/net/flashbots/models/bundle/SimBundleResponse.java
@@ -0,0 +1,239 @@
+package net.flashbots.models.bundle;
+
+import java.math.BigInteger;
+import java.util.List;
+import java.util.Objects;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import net.flashbots.provider.json.HexStringToBigIntDeserializer;
+
+/**
+ * The type SimBundleResponse
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class SimBundleResponse {
+
+ private Boolean success;
+
+ private String error;
+
+ @JsonDeserialize(using = HexStringToBigIntDeserializer.class)
+ private BigInteger stateBlock;
+
+ @JsonDeserialize(using = HexStringToBigIntDeserializer.class)
+ private BigInteger mevGasPrice;
+
+ @JsonDeserialize(using = HexStringToBigIntDeserializer.class)
+ private BigInteger profit;
+
+ @JsonDeserialize(using = HexStringToBigIntDeserializer.class)
+ private BigInteger refundableValue;
+
+ @JsonDeserialize(using = HexStringToBigIntDeserializer.class)
+ private BigInteger gasUsed;
+
+ private List logs;
+
+ /**
+ * Instantiates a new simulate bundle response
+ */
+ public SimBundleResponse() {}
+
+ /**
+ * Gets the success flag
+ *
+ * @return the success flag
+ */
+ public Boolean getSuccess() {
+ return success;
+ }
+
+ /**
+ * Sets success flag
+ *
+ * @param success the success flag
+ * @return this simulates bundle response
+ */
+ public SimBundleResponse setSuccess(Boolean success) {
+ this.success = success;
+ return this;
+ }
+
+ /**
+ * Gets the error info
+ *
+ * @return the error info
+ */
+ public String getError() {
+ return error;
+ }
+
+ /**
+ * Sets the error info
+ *
+ * @param error the error info
+ * @return this simulates bundle response
+ */
+ public SimBundleResponse setError(String error) {
+ this.error = error;
+ return this;
+ }
+
+ /**
+ * Gets the state block
+ *
+ * @return the state block
+ */
+ public BigInteger getStateBlock() {
+ return stateBlock;
+ }
+
+ /**
+ * Sets state block
+ *
+ * @param stateBlock the state block
+ * @return this simulates bundle response
+ */
+ public SimBundleResponse setStateBlock(BigInteger stateBlock) {
+ this.stateBlock = stateBlock;
+ return this;
+ }
+
+ /**
+ * Gets the mev-share gas price
+ *
+ * @return the mev-share gas price
+ */
+ public BigInteger getMevGasPrice() {
+ return mevGasPrice;
+ }
+
+ /**
+ * Sets mev-share gas price
+ *
+ * @param mevGasPrice the mev-share gas price
+ * @return this simulates bundle response
+ */
+ public SimBundleResponse setMevGasPrice(BigInteger mevGasPrice) {
+ this.mevGasPrice = mevGasPrice;
+ return this;
+ }
+
+ /**
+ * Gets the profit
+ *
+ * @return the profit
+ */
+ public BigInteger getProfit() {
+ return profit;
+ }
+
+ /**
+ * Sets profit
+ *
+ * @param profit the profit
+ * @return this simulates bundle response
+ */
+ public SimBundleResponse setProfit(BigInteger profit) {
+ this.profit = profit;
+ return this;
+ }
+
+ /**
+ * Gets the refundable value
+ *
+ * @return the refundable value
+ */
+ public BigInteger getRefundableValue() {
+ return refundableValue;
+ }
+
+ /**
+ * Sets refundable value
+ *
+ * @param refundableValue the refundable value
+ * @return this simulates bundle response
+ */
+ public SimBundleResponse setRefundableValue(BigInteger refundableValue) {
+ this.refundableValue = refundableValue;
+ return this;
+ }
+
+ /**
+ * Gets the gas used
+ *
+ * @return the gas used
+ */
+ public BigInteger getGasUsed() {
+ return gasUsed;
+ }
+
+ /**
+ * Sets gas used
+ *
+ * @param gasUsed the gas used
+ * @return this simulates bundle response
+ */
+ public SimBundleResponse setGasUsed(BigInteger gasUsed) {
+ this.gasUsed = gasUsed;
+ return this;
+ }
+
+ /**
+ * Gets the list of simulate bundle logs
+ *
+ * @return the list of simulate bundle logs
+ */
+ public List getLogs() {
+ return logs;
+ }
+
+ /**
+ * Sets list of simulate bundle logs
+ *
+ * @param logs the list of simulate bundle logs
+ * @return this simulates bundle response
+ */
+ public SimBundleResponse setLogs(List logs) {
+ this.logs = logs;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SimBundleResponse that)) {
+ return false;
+ }
+ return Objects.equals(success, that.success)
+ && Objects.equals(error, that.error)
+ && Objects.equals(stateBlock, that.stateBlock)
+ && Objects.equals(mevGasPrice, that.mevGasPrice)
+ && Objects.equals(profit, that.profit)
+ && Objects.equals(refundableValue, that.refundableValue)
+ && Objects.equals(gasUsed, that.gasUsed)
+ && Objects.equals(logs, that.logs);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(success, error, stateBlock, mevGasPrice, profit, refundableValue, gasUsed, logs);
+ }
+
+ @Override
+ public String toString() {
+ return "SimBundleResponse{" + "success="
+ + success + ", error='"
+ + error + '\'' + ", stateBlock="
+ + stateBlock + ", mevGasPrice="
+ + mevGasPrice + ", profit="
+ + profit + ", refundableValue="
+ + refundableValue + ", gasUsed="
+ + gasUsed + ", logs="
+ + logs + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/models/bundle/Validity.java b/src/main/java/net/flashbots/models/bundle/Validity.java
new file mode 100644
index 0000000..a00d2e2
--- /dev/null
+++ b/src/main/java/net/flashbots/models/bundle/Validity.java
@@ -0,0 +1,77 @@
+package net.flashbots.models.bundle;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * The type Validity.
+ */
+public class Validity {
+
+ private List refundConfig = new ArrayList<>();
+
+ private List refund = new ArrayList<>();
+
+ /**
+ * Instantiates a new Validity.
+ */
+ public Validity() {}
+
+ /**
+ * Gets refund config.
+ *
+ * @return the refund config
+ */
+ public List getRefundConfig() {
+ return refundConfig;
+ }
+
+ /**
+ * Sets refund config.
+ *
+ * @param refundConfig the refund config
+ * @return the refund config
+ */
+ public Validity setRefundConfig(List refundConfig) {
+ this.refundConfig = refundConfig;
+ return this;
+ }
+
+ /**
+ * Gets refund.
+ *
+ * @return the refund
+ */
+ public List getRefund() {
+ return refund;
+ }
+
+ /**
+ * Sets refund.
+ *
+ * @param refund the refund
+ * @return the refund
+ */
+ public Validity setRefund(List refund) {
+ this.refund = refund;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Validity validity)) return false;
+ return Objects.equals(refundConfig, validity.refundConfig) && Objects.equals(refund, validity.refund);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(refundConfig, refund);
+ }
+
+ @Override
+ public String toString() {
+ return "Validity{" + "refundConfig=" + refundConfig + ", refund=" + refund + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/models/common/JsonRpc20Request.java b/src/main/java/net/flashbots/models/common/JsonRpc20Request.java
new file mode 100644
index 0000000..d03855e
--- /dev/null
+++ b/src/main/java/net/flashbots/models/common/JsonRpc20Request.java
@@ -0,0 +1,130 @@
+package net.flashbots.models.common;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * The type JsonRpc20Request.
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class JsonRpc20Request {
+
+ private String jsonrpc = "2.0";
+
+ private String method;
+
+ private List> params;
+
+ private Long id;
+
+ /**
+ * Instantiates a new Json rpc 20 request.
+ */
+ public JsonRpc20Request() {}
+
+ /**
+ * Gets jsonrpc.
+ *
+ * @return the jsonrpc
+ */
+ public String getJsonrpc() {
+ return jsonrpc;
+ }
+
+ /**
+ * Sets jsonrpc.
+ *
+ * @param jsonrpc the jsonrpc
+ * @return the jsonrpc
+ */
+ public JsonRpc20Request setJsonrpc(String jsonrpc) {
+ this.jsonrpc = jsonrpc;
+ return this;
+ }
+
+ /**
+ * Gets method.
+ *
+ * @return the method
+ */
+ public String getMethod() {
+ return method;
+ }
+
+ /**
+ * Sets method.
+ *
+ * @param method the method
+ * @return the method
+ */
+ public JsonRpc20Request setMethod(String method) {
+ this.method = method;
+ return this;
+ }
+
+ /**
+ * Gets params.
+ *
+ * @return the params
+ */
+ public List> getParams() {
+ return params;
+ }
+
+ /**
+ * Sets params.
+ *
+ * @param params the params
+ * @return the params
+ */
+ public JsonRpc20Request setParams(List> params) {
+ this.params = params;
+ return this;
+ }
+
+ /**
+ * Gets id.
+ *
+ * @return the id
+ */
+ public Long getId() {
+ return id;
+ }
+
+ /**
+ * Sets id.
+ *
+ * @param id the id
+ * @return the id
+ */
+ public JsonRpc20Request setId(Long id) {
+ this.id = id;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof JsonRpc20Request request)) return false;
+ return Objects.equals(jsonrpc, request.jsonrpc)
+ && Objects.equals(method, request.method)
+ && Objects.equals(params, request.params)
+ && Objects.equals(id, request.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(jsonrpc, method, params, id);
+ }
+
+ @Override
+ public String toString() {
+ return "JsonRpc20Request{" + "jsonrpc='"
+ + jsonrpc + '\'' + ", method='"
+ + method + '\'' + ", params="
+ + params + ", id="
+ + id + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/models/common/JsonRpc20Response.java b/src/main/java/net/flashbots/models/common/JsonRpc20Response.java
new file mode 100644
index 0000000..cd33c1b
--- /dev/null
+++ b/src/main/java/net/flashbots/models/common/JsonRpc20Response.java
@@ -0,0 +1,236 @@
+package net.flashbots.models.common;
+
+import java.util.Objects;
+
+/**
+ * The type JsonRpc20Response.
+ *
+ * @param the type parameter
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class JsonRpc20Response {
+
+ /** The type JsonRpcError. */
+ public static class JsonRpcError {
+
+ private Integer code;
+
+ private String message;
+
+ /**
+ * Instantiates a new JsonRpcError.
+ */
+ public JsonRpcError() {}
+
+ /**
+ * Instantiates a new JsonRpcError.
+ *
+ * @param message error message
+ */
+ public JsonRpcError(String message) {
+ this.message = message;
+ }
+
+ /**
+ * Gets code.
+ *
+ * @return the code
+ */
+ public Integer getCode() {
+ return code;
+ }
+
+ /**
+ * Sets code.
+ *
+ * @param code the code
+ */
+ public void setCode(Integer code) {
+ this.code = code;
+ }
+
+ /**
+ * Gets message.
+ *
+ * @return the message
+ */
+ public String getMessage() {
+ return message;
+ }
+
+ /**
+ * Sets message.
+ *
+ * @param message the message
+ */
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof JsonRpcError that)) return false;
+ return Objects.equals(code, that.code) && Objects.equals(message, that.message);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(code, message);
+ }
+
+ @Override
+ public String toString() {
+ return "JsonRpcError{" + "code=" + code + ", message='" + message + '\'' + '}';
+ }
+ }
+
+ private Long id;
+
+ private String jsonrpc = "2.0";
+
+ private T result;
+
+ private JsonRpcError jsonRpcError;
+
+ private Throwable throwable;
+
+ /**
+ * Instantiates a new Json rpc 20 response.
+ */
+ public JsonRpc20Response() {}
+
+ /**
+ * Gets id.
+ *
+ * @return the id
+ */
+ public Long getId() {
+ return id;
+ }
+
+ /**
+ * Sets id.
+ *
+ * @param id the id
+ * @return the id
+ */
+ public JsonRpc20Response setId(Long id) {
+ this.id = id;
+ return this;
+ }
+
+ /**
+ * Gets jsonrpc.
+ *
+ * @return the jsonrpc
+ */
+ public String getJsonrpc() {
+ return jsonrpc;
+ }
+
+ /**
+ * Sets jsonrpc.
+ *
+ * @param jsonrpc the jsonrpc
+ * @return the jsonrpc
+ */
+ public JsonRpc20Response setJsonrpc(String jsonrpc) {
+ this.jsonrpc = jsonrpc;
+ return this;
+ }
+
+ /**
+ * Gets result.
+ *
+ * @return the result
+ */
+ public T getResult() {
+ return result;
+ }
+
+ /**
+ * Sets result.
+ *
+ * @param result the result
+ * @return the result
+ */
+ public JsonRpc20Response setResult(T result) {
+ this.result = result;
+ return this;
+ }
+
+ /**
+ * Gets jsonRpcError.
+ *
+ * @return the jsonRpcError
+ */
+ public JsonRpcError getError() {
+ return jsonRpcError;
+ }
+
+ /**
+ * Sets jsonRpcError.
+ *
+ * @param jsonRpcError the jsonRpcError
+ * @return the jsonRpcError
+ */
+ public JsonRpc20Response setError(JsonRpcError jsonRpcError) {
+ this.jsonRpcError = jsonRpcError;
+ return this;
+ }
+
+ /**
+ * Gets throwable.
+ *
+ * @return the throwable
+ */
+ public Throwable getThrowable() {
+ return throwable;
+ }
+
+ /**
+ * Sets throwable.
+ *
+ * @param throwable the throwable
+ * @return the throwable
+ */
+ public JsonRpc20Response setThrowable(Throwable throwable) {
+ this.throwable = throwable;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof JsonRpc20Response> that)) return false;
+ return Objects.equals(id, that.id)
+ && Objects.equals(jsonrpc, that.jsonrpc)
+ && Objects.equals(result, that.result)
+ && Objects.equals(jsonRpcError, that.jsonRpcError)
+ && Objects.equals(throwable, that.throwable);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, jsonrpc, result, jsonRpcError, throwable);
+ }
+
+ @Override
+ public String toString() {
+ return "JsonRpc20Response{"
+ + "id="
+ + id
+ + ", jsonrpc='"
+ + jsonrpc
+ + '\''
+ + ", result="
+ + result
+ + ", jsonRpcError="
+ + jsonRpcError
+ + ", throwable="
+ + throwable
+ + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/models/common/Network.java b/src/main/java/net/flashbots/models/common/Network.java
new file mode 100644
index 0000000..0a6ba9d
--- /dev/null
+++ b/src/main/java/net/flashbots/models/common/Network.java
@@ -0,0 +1,147 @@
+package net.flashbots.models.common;
+
+import java.util.Objects;
+
+/**
+ * The type Network.
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class Network {
+
+ /**
+ * The constant MAINNET.
+ */
+ public static final Network MAINNET = new Network()
+ .setName("mainnet")
+ .setChainId(1)
+ .setRpcUrl("https://relay.flashbots.net")
+ .setStreamUrl("https://mev-share.flashbots.net");
+
+ /**
+ * The constant GOERLI.
+ */
+ public static final Network GOERLI = new Network()
+ .setName("goerli")
+ .setChainId(5)
+ .setRpcUrl("https://relay-goerli.flashbots.net")
+ .setStreamUrl("https://mev-share-goerli.flashbots.net");
+
+ /**
+ * Instantiates a new Network.
+ */
+ public Network() {}
+
+ private String name;
+
+ private long chainId;
+
+ private String rpcUrl;
+
+ private String streamUrl;
+
+ /**
+ * Name string.
+ *
+ * @return the string
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * Sets name.
+ *
+ * @param name the name
+ * @return the name
+ */
+ public Network setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /**
+ * Chain id long.
+ *
+ * @return the long
+ */
+ public long chainId() {
+ return chainId;
+ }
+
+ /**
+ * Sets chain id.
+ *
+ * @param chainId the chain id
+ * @return the chain id
+ */
+ public Network setChainId(long chainId) {
+ this.chainId = chainId;
+ return this;
+ }
+
+ /**
+ * Rpc url string.
+ *
+ * @return the string
+ */
+ public String rpcUrl() {
+ return rpcUrl;
+ }
+
+ /**
+ * Sets rpc url.
+ *
+ * @param rpcUrl the rpc url
+ * @return the rpc url
+ */
+ public Network setRpcUrl(String rpcUrl) {
+ this.rpcUrl = rpcUrl;
+ return this;
+ }
+
+ /**
+ * Stream url string.
+ *
+ * @return the string
+ */
+ public String streamUrl() {
+ return streamUrl;
+ }
+
+ /**
+ * Sets stream url.
+ *
+ * @param streamUrl the stream url
+ * @return the stream url
+ */
+ public Network setStreamUrl(String streamUrl) {
+ this.streamUrl = streamUrl;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Network that)) return false;
+ return chainId == that.chainId
+ && Objects.equals(name, that.name)
+ && Objects.equals(rpcUrl, that.rpcUrl)
+ && Objects.equals(streamUrl, that.streamUrl);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, chainId, rpcUrl, streamUrl);
+ }
+
+ @Override
+ public String toString() {
+ return "Network{" + "name='"
+ + name + '\'' + ", chainId="
+ + chainId + ", rpcUrl='"
+ + rpcUrl + '\'' + ", streamUrl='"
+ + streamUrl + '\'' + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/models/event/EventHistoryEntry.java b/src/main/java/net/flashbots/models/event/EventHistoryEntry.java
new file mode 100644
index 0000000..4af392f
--- /dev/null
+++ b/src/main/java/net/flashbots/models/event/EventHistoryEntry.java
@@ -0,0 +1,103 @@
+package net.flashbots.models.event;
+
+import java.math.BigInteger;
+import java.util.Objects;
+
+/**
+ * The type EventHistoryEntry.
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class EventHistoryEntry {
+
+ private BigInteger block;
+
+ private BigInteger timestamp;
+
+ private MevShareEvent hint;
+
+ /**
+ * Instantiates a new EventHistoryEntry.
+ */
+ public EventHistoryEntry() {}
+
+ /**
+ * Gets block.
+ *
+ * @return the block
+ */
+ public BigInteger getBlock() {
+ return block;
+ }
+
+ /**
+ * Sets block.
+ *
+ * @param block the block
+ * @return the block
+ */
+ public EventHistoryEntry setBlock(BigInteger block) {
+ this.block = block;
+ return this;
+ }
+
+ /**
+ * Gets timestamp.
+ *
+ * @return the timestamp
+ */
+ public BigInteger getTimestamp() {
+ return timestamp;
+ }
+
+ /**
+ * Sets timestamp.
+ *
+ * @param timestamp the timestamp
+ * @return the timestamp
+ */
+ public EventHistoryEntry setTimestamp(BigInteger timestamp) {
+ this.timestamp = timestamp;
+ return this;
+ }
+
+ /**
+ * Gets hint.
+ *
+ * @return the hint
+ */
+ public MevShareEvent getHint() {
+ return hint;
+ }
+
+ /**
+ * Sets hint.
+ *
+ * @param hint the hint
+ * @return the hint
+ */
+ public EventHistoryEntry setHint(MevShareEvent hint) {
+ this.hint = hint;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof EventHistoryEntry that)) return false;
+ return Objects.equals(block, that.block)
+ && Objects.equals(timestamp, that.timestamp)
+ && Objects.equals(hint, that.hint);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(block, timestamp, hint);
+ }
+
+ @Override
+ public String toString() {
+ return "EventHistoryEntry{" + "block=" + block + ", timestamp=" + timestamp + ", hint=" + hint + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/models/event/EventHistoryInfo.java b/src/main/java/net/flashbots/models/event/EventHistoryInfo.java
new file mode 100644
index 0000000..123f570
--- /dev/null
+++ b/src/main/java/net/flashbots/models/event/EventHistoryInfo.java
@@ -0,0 +1,178 @@
+package net.flashbots.models.event;
+
+import java.math.BigInteger;
+import java.util.Objects;
+
+/**
+ * The type EventHistoryInfo.
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class EventHistoryInfo {
+
+ private BigInteger count;
+
+ private BigInteger minBlock;
+
+ private BigInteger maxBlock;
+
+ private BigInteger minTimestamp;
+
+ private BigInteger maxTimestamp;
+
+ private BigInteger maxLimit;
+
+ /**
+ * Instantiates a new EventHistoryInfo.
+ */
+ public EventHistoryInfo() {}
+
+ /**
+ * Gets count.
+ *
+ * @return the count
+ */
+ public BigInteger getCount() {
+ return count;
+ }
+
+ /**
+ * Sets count.
+ *
+ * @param count the count
+ * @return the count
+ */
+ public EventHistoryInfo setCount(BigInteger count) {
+ this.count = count;
+ return this;
+ }
+
+ /**
+ * Gets min block.
+ *
+ * @return the min block
+ */
+ public BigInteger getMinBlock() {
+ return minBlock;
+ }
+
+ /**
+ * Sets min block.
+ *
+ * @param minBlock the min block
+ * @return the min block
+ */
+ public EventHistoryInfo setMinBlock(BigInteger minBlock) {
+ this.minBlock = minBlock;
+ return this;
+ }
+
+ /**
+ * Gets max block.
+ *
+ * @return the max block
+ */
+ public BigInteger getMaxBlock() {
+ return maxBlock;
+ }
+
+ /**
+ * Sets max block.
+ *
+ * @param maxBlock the max block
+ * @return the max block
+ */
+ public EventHistoryInfo setMaxBlock(BigInteger maxBlock) {
+ this.maxBlock = maxBlock;
+ return this;
+ }
+
+ /**
+ * Gets min timestamp.
+ *
+ * @return the min timestamp
+ */
+ public BigInteger getMinTimestamp() {
+ return minTimestamp;
+ }
+
+ /**
+ * Sets min timestamp.
+ *
+ * @param minTimestamp the min timestamp
+ * @return the min timestamp
+ */
+ public EventHistoryInfo setMinTimestamp(BigInteger minTimestamp) {
+ this.minTimestamp = minTimestamp;
+ return this;
+ }
+
+ /**
+ * Gets max timestamp.
+ *
+ * @return the max timestamp
+ */
+ public BigInteger getMaxTimestamp() {
+ return maxTimestamp;
+ }
+
+ /**
+ * Sets max timestamp.
+ *
+ * @param maxTimestamp the max timestamp
+ * @return the max timestamp
+ */
+ public EventHistoryInfo setMaxTimestamp(BigInteger maxTimestamp) {
+ this.maxTimestamp = maxTimestamp;
+ return this;
+ }
+
+ /**
+ * Gets max limit.
+ *
+ * @return the max limit
+ */
+ public BigInteger getMaxLimit() {
+ return maxLimit;
+ }
+
+ /**
+ * Sets max limit.
+ *
+ * @param maxLimit the max limit
+ * @return the max limit
+ */
+ public EventHistoryInfo setMaxLimit(BigInteger maxLimit) {
+ this.maxLimit = maxLimit;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof EventHistoryInfo that)) return false;
+ return Objects.equals(count, that.count)
+ && Objects.equals(minBlock, that.minBlock)
+ && Objects.equals(maxBlock, that.maxBlock)
+ && Objects.equals(minTimestamp, that.minTimestamp)
+ && Objects.equals(maxTimestamp, that.maxTimestamp)
+ && Objects.equals(maxLimit, that.maxLimit);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(count, minBlock, maxBlock, minTimestamp, maxTimestamp, maxLimit);
+ }
+
+ @Override
+ public String toString() {
+ return "EventHistoryInfo{" + "count="
+ + count + ", minBlock="
+ + minBlock + ", maxBlock="
+ + maxBlock + ", minTimestamp="
+ + minTimestamp + ", maxTimestamp="
+ + maxTimestamp + ", maxLimit="
+ + maxLimit + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/models/event/EventHistoryParams.java b/src/main/java/net/flashbots/models/event/EventHistoryParams.java
new file mode 100644
index 0000000..bef686f
--- /dev/null
+++ b/src/main/java/net/flashbots/models/event/EventHistoryParams.java
@@ -0,0 +1,178 @@
+package net.flashbots.models.event;
+
+import java.math.BigInteger;
+import java.util.Objects;
+
+/**
+ * The type EventHistoryParams.
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class EventHistoryParams {
+
+ private BigInteger blockStart;
+
+ private BigInteger blockEnd;
+
+ private BigInteger timestampStart;
+
+ private BigInteger timestampEnd;
+
+ private Integer limit;
+
+ private Integer offset;
+
+ /**
+ * Instantiates a new EventHistoryParams.
+ */
+ public EventHistoryParams() {}
+
+ /**
+ * Gets block start.
+ *
+ * @return the block start
+ */
+ public BigInteger getBlockStart() {
+ return blockStart;
+ }
+
+ /**
+ * Sets block start.
+ *
+ * @param blockStart the block start
+ * @return the block start
+ */
+ public EventHistoryParams setBlockStart(BigInteger blockStart) {
+ this.blockStart = blockStart;
+ return this;
+ }
+
+ /**
+ * Gets block end.
+ *
+ * @return the block end
+ */
+ public BigInteger getBlockEnd() {
+ return blockEnd;
+ }
+
+ /**
+ * Sets block end.
+ *
+ * @param blockEnd the block end
+ * @return the block end
+ */
+ public EventHistoryParams setBlockEnd(BigInteger blockEnd) {
+ this.blockEnd = blockEnd;
+ return this;
+ }
+
+ /**
+ * Gets timestamp start.
+ *
+ * @return the timestamp start
+ */
+ public BigInteger getTimestampStart() {
+ return timestampStart;
+ }
+
+ /**
+ * Sets timestamp start.
+ *
+ * @param timestampStart the timestamp start
+ * @return the timestamp start
+ */
+ public EventHistoryParams setTimestampStart(BigInteger timestampStart) {
+ this.timestampStart = timestampStart;
+ return this;
+ }
+
+ /**
+ * Gets timestamp end.
+ *
+ * @return the timestamp end
+ */
+ public BigInteger getTimestampEnd() {
+ return timestampEnd;
+ }
+
+ /**
+ * Sets timestamp end.
+ *
+ * @param timestampEnd the timestamp end
+ * @return the timestamp end
+ */
+ public EventHistoryParams setTimestampEnd(BigInteger timestampEnd) {
+ this.timestampEnd = timestampEnd;
+ return this;
+ }
+
+ /**
+ * Gets limit.
+ *
+ * @return the limit
+ */
+ public Integer getLimit() {
+ return limit;
+ }
+
+ /**
+ * Sets limit.
+ *
+ * @param limit the limit
+ * @return the limit
+ */
+ public EventHistoryParams setLimit(Integer limit) {
+ this.limit = limit;
+ return this;
+ }
+
+ /**
+ * Gets offset.
+ *
+ * @return the offset
+ */
+ public Integer getOffset() {
+ return offset;
+ }
+
+ /**
+ * Sets offset.
+ *
+ * @param offset the offset
+ * @return the offset
+ */
+ public EventHistoryParams setOffset(Integer offset) {
+ this.offset = offset;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof EventHistoryParams that)) return false;
+ return Objects.equals(blockStart, that.blockStart)
+ && Objects.equals(blockEnd, that.blockEnd)
+ && Objects.equals(timestampStart, that.timestampStart)
+ && Objects.equals(timestampEnd, that.timestampEnd)
+ && Objects.equals(limit, that.limit)
+ && Objects.equals(offset, that.offset);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(blockStart, blockEnd, timestampStart, timestampEnd, limit, offset);
+ }
+
+ @Override
+ public String toString() {
+ return "EventHistoryParams{" + "blockStart="
+ + blockStart + ", blockEnd="
+ + blockEnd + ", timestampStart="
+ + timestampStart + ", timestampEnd="
+ + timestampEnd + ", limit="
+ + limit + ", offset="
+ + offset + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/models/event/MevShareEvent.java b/src/main/java/net/flashbots/models/event/MevShareEvent.java
new file mode 100644
index 0000000..2486035
--- /dev/null
+++ b/src/main/java/net/flashbots/models/event/MevShareEvent.java
@@ -0,0 +1,161 @@
+package net.flashbots.models.event;
+
+import java.math.BigInteger;
+import java.util.List;
+import java.util.Objects;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import net.flashbots.provider.json.HexStringToBigIntDeserializer;
+import org.web3j.protocol.core.methods.response.EthLog;
+
+/**
+ * The type MevShareEvent.
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class MevShareEvent {
+ private List txs;
+
+ private String hash;
+
+ @JsonDeserialize(using = EthLog.LogResultDeserialiser.class)
+ private List> logs;
+
+ @JsonDeserialize(using = HexStringToBigIntDeserializer.class)
+ private BigInteger gasUsed;
+
+ @JsonDeserialize(using = HexStringToBigIntDeserializer.class)
+ private BigInteger mevGasPrice;
+
+ /**
+ * Instantiates a new MevShareEvent.
+ */
+ public MevShareEvent() {}
+
+ /**
+ * Gets txs.
+ *
+ * @return the txs
+ */
+ public List getTxs() {
+ return txs;
+ }
+
+ /**
+ * Sets txs.
+ *
+ * @param txs the txs
+ * @return the txs
+ */
+ public MevShareEvent setTxs(List txs) {
+ this.txs = txs;
+ return this;
+ }
+
+ /**
+ * Gets hash.
+ *
+ * @return the hash
+ */
+ public String getHash() {
+ return hash;
+ }
+
+ /**
+ * Sets hash.
+ *
+ * @param hash the hash
+ * @return the hash
+ */
+ public MevShareEvent setHash(String hash) {
+ this.hash = hash;
+ return this;
+ }
+
+ /**
+ * Gets logs.
+ *
+ * @return the logs
+ */
+ public List> getLogs() {
+ return logs;
+ }
+
+ /**
+ * Sets logs.
+ *
+ * @param logs the logs
+ * @return the logs
+ */
+ public MevShareEvent setLogs(List> logs) {
+ this.logs = logs;
+ return this;
+ }
+
+ /**
+ * Gets gas used.
+ *
+ * @return the gas used
+ */
+ public BigInteger getGasUsed() {
+ return gasUsed;
+ }
+
+ /**
+ * Sets gas used.
+ *
+ * @param gasUsed the gas used
+ * @return the gas used
+ */
+ public MevShareEvent setGasUsed(BigInteger gasUsed) {
+ this.gasUsed = gasUsed;
+ return this;
+ }
+
+ /**
+ * Gets mev gas price.
+ *
+ * @return the mev gas price
+ */
+ public BigInteger getMevGasPrice() {
+ return mevGasPrice;
+ }
+
+ /**
+ * Sets mev gas price.
+ *
+ * @param mevGasPrice the mev gas price
+ * @return the mev gas price
+ */
+ public MevShareEvent setMevGasPrice(BigInteger mevGasPrice) {
+ this.mevGasPrice = mevGasPrice;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof MevShareEvent mevShareEvent)) return false;
+ return Objects.equals(gasUsed, mevShareEvent.gasUsed)
+ && Objects.equals(mevGasPrice, mevShareEvent.mevGasPrice)
+ && Objects.equals(txs, mevShareEvent.txs)
+ && Objects.equals(hash, mevShareEvent.hash)
+ && Objects.equals(logs, mevShareEvent.logs);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(txs, hash, logs, gasUsed, mevGasPrice);
+ }
+
+ @Override
+ public String toString() {
+ return "MevShareEvent{" + "txs="
+ + txs + ", hash='"
+ + hash + '\'' + ", logs="
+ + logs + ", gasUsed="
+ + gasUsed + ", mevGasPrice="
+ + mevGasPrice + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/models/event/Transaction.java b/src/main/java/net/flashbots/models/event/Transaction.java
new file mode 100644
index 0000000..a720dd6
--- /dev/null
+++ b/src/main/java/net/flashbots/models/event/Transaction.java
@@ -0,0 +1,158 @@
+package net.flashbots.models.event;
+
+import java.math.BigInteger;
+import java.util.Objects;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import net.flashbots.provider.json.HexStringToBigIntDeserializer;
+
+/**
+ * The type Transaction.
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class Transaction {
+ private String to;
+
+ private String callData;
+
+ private String functionSelector;
+
+ @JsonDeserialize(using = HexStringToBigIntDeserializer.class)
+ private BigInteger mevGasPrice;
+
+ @JsonDeserialize(using = HexStringToBigIntDeserializer.class)
+ private BigInteger gasUsed;
+
+ /**
+ * Instantiates a new Transaction.
+ */
+ public Transaction() {}
+
+ /**
+ * Gets to.
+ *
+ * @return the to
+ */
+ public String getTo() {
+ return to;
+ }
+
+ /**
+ * Sets to.
+ *
+ * @param to the to
+ * @return the to
+ */
+ public Transaction setTo(String to) {
+ this.to = to;
+ return this;
+ }
+
+ /**
+ * Gets call data.
+ *
+ * @return the call data
+ */
+ public String getCallData() {
+ return callData;
+ }
+
+ /**
+ * Sets call data.
+ *
+ * @param callData the call data
+ * @return the call data
+ */
+ public Transaction setCallData(String callData) {
+ this.callData = callData;
+ return this;
+ }
+
+ /**
+ * Gets function selector.
+ *
+ * @return the function selector
+ */
+ public String getFunctionSelector() {
+ return functionSelector;
+ }
+
+ /**
+ * Sets function selector.
+ *
+ * @param functionSelector the function selector
+ * @return the function selector
+ */
+ public Transaction setFunctionSelector(String functionSelector) {
+ this.functionSelector = functionSelector;
+ return this;
+ }
+
+ /**
+ * Gets mev gas price.
+ *
+ * @return the mev gas price
+ */
+ public BigInteger getMevGasPrice() {
+ return mevGasPrice;
+ }
+
+ /**
+ * Sets mev gas price.
+ *
+ * @param mevGasPrice the mev gas price
+ * @return the mev gas price
+ */
+ public Transaction setMevGasPrice(BigInteger mevGasPrice) {
+ this.mevGasPrice = mevGasPrice;
+ return this;
+ }
+
+ /**
+ * Gets gas used.
+ *
+ * @return the gas used
+ */
+ public BigInteger getGasUsed() {
+ return gasUsed;
+ }
+
+ /**
+ * Sets gas used.
+ *
+ * @param gasUsed the gas used
+ * @return the gas used
+ */
+ public Transaction setGasUsed(BigInteger gasUsed) {
+ this.gasUsed = gasUsed;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Transaction that)) return false;
+ return Objects.equals(to, that.to)
+ && Objects.equals(callData, that.callData)
+ && Objects.equals(functionSelector, that.functionSelector)
+ && Objects.equals(mevGasPrice, that.mevGasPrice)
+ && Objects.equals(gasUsed, that.gasUsed);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(to, callData, functionSelector, mevGasPrice, gasUsed);
+ }
+
+ @Override
+ public String toString() {
+ return "Transaction{" + "to='"
+ + to + '\'' + ", callData='"
+ + callData + '\'' + ", functionSelector='"
+ + functionSelector + '\'' + ", mevGasPrice="
+ + mevGasPrice + ", gasUsed="
+ + gasUsed + '}';
+ }
+}
diff --git a/src/main/java/net/flashbots/provider/HttpProvider.java b/src/main/java/net/flashbots/provider/HttpProvider.java
new file mode 100644
index 0000000..4d378c1
--- /dev/null
+++ b/src/main/java/net/flashbots/provider/HttpProvider.java
@@ -0,0 +1,193 @@
+package net.flashbots.provider;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import net.flashbots.common.MevShareApiException;
+import net.flashbots.models.common.JsonRpc20Request;
+import net.flashbots.models.common.JsonRpc20Response;
+import okhttp3.*;
+import okhttp3.sse.EventSource;
+import okhttp3.sse.EventSources;
+import org.slf4j.Logger;
+import org.web3j.crypto.Credentials;
+import org.web3j.crypto.Hash;
+import org.web3j.crypto.Keys;
+import org.web3j.crypto.Sign;
+import org.web3j.utils.Numeric;
+
+/**
+ * The type HttpProvider.
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class HttpProvider {
+
+ private static final Logger LOGGER = getLogger(HttpProvider.class);
+ private final OkHttpClient httpClient;
+
+ private final EventSource.Factory eventSourceFactory;
+ private final ObjectMapper objectMapper;
+
+ private final AtomicLong nextId = new AtomicLong();
+
+ /**
+ * Instantiates a new HttpProvider.
+ * @param objectMapper the object mapper
+ */
+ public HttpProvider(ObjectMapper objectMapper) {
+ this.objectMapper = objectMapper;
+ this.httpClient = new OkHttpClient()
+ .newBuilder()
+ .connectTimeout(Duration.ofSeconds(30))
+ .writeTimeout(Duration.ofSeconds(30))
+ .readTimeout(Duration.ofSeconds(30))
+ .build();
+ this.eventSourceFactory = EventSources.createFactory(this.httpClient);
+ }
+
+ /**
+ * Send CompletableFuture.
+ *
+ * @param the type parameter
+ * @param request the request
+ * @param respType the respType
+ * @return the completable future
+ */
+ public CompletableFuture send(Request request, JavaType respType) {
+ LOGGER.trace("Sending request: {}", request);
+ CompletableFuture completableFuture = new CompletableFuture<>();
+ this.httpClient.newCall(request).enqueue(new Callback() {
+ @Override
+ public void onFailure(Call call, IOException e) {
+ LOGGER.error("JsonRpcError sending request", e);
+ completableFuture.completeExceptionally(e);
+ }
+
+ @Override
+ public void onResponse(Call call, Response response) {
+ try {
+ if (response.body() != null) {
+ String respBody = response.body().string();
+ LOGGER.trace("Received response: {}", respBody);
+ T res = objectMapper.readValue(respBody, respType);
+ completableFuture.complete(res);
+ }
+ } catch (IOException | IllegalStateException e) {
+ LOGGER.error("JsonRpcError parsing response", e);
+ completableFuture.completeExceptionally(e);
+ }
+ }
+ });
+
+ return completableFuture;
+ }
+
+ /**
+ * Send completable future.
+ *
+ * @param the type parameter
+ * @param url the url
+ * @param request the request
+ * @param authSigner the auth signer
+ * @param respType the resp type
+ * @return the completable future
+ */
+ @SuppressWarnings("unchecked")
+ public CompletableFuture send(
+ String url, JsonRpc20Request request, Credentials authSigner, JavaType respType) {
+ final CompletableFuture future = new CompletableFuture<>();
+ String requestBodyJson;
+ try {
+ requestBodyJson = objectMapper.writeValueAsString(request);
+ LOGGER.trace("request body: {}", requestBodyJson);
+ } catch (JsonProcessingException e) {
+ LOGGER.error("JsonRpcError serializing request", e);
+ future.completeExceptionally(e);
+ return future;
+ }
+
+ Sign.SignatureData signatureData = Sign.signPrefixedMessage(
+ Hash.sha3String(requestBodyJson).getBytes(StandardCharsets.UTF_8), authSigner.getEcKeyPair());
+ byte[] signatureBytes = new byte[65];
+ System.arraycopy(signatureData.getR(), 0, signatureBytes, 0, 32);
+ System.arraycopy(signatureData.getS(), 0, signatureBytes, 32, 32);
+ signatureBytes[64] = signatureData.getV()[0];
+ String signature = String.format(
+ "%s:%s",
+ Numeric.prependHexPrefix(
+ Keys.getAddress(authSigner.getEcKeyPair().getPublicKey())),
+ Numeric.toHexString(signatureBytes));
+ LOGGER.trace("signature: {}", signature);
+ final RequestBody requestBody =
+ RequestBody.create(requestBodyJson, MediaType.get("application/json; charset=utf-8"));
+ Request okhttpRequest = new Request.Builder()
+ .url(url)
+ .post(requestBody)
+ .addHeader("X-Flashbots-Signature", signature)
+ .addHeader("Content-Type", "application/json; charset=utf-8")
+ .build();
+ return send(
+ okhttpRequest,
+ objectMapper.getTypeFactory().constructParametricType(JsonRpc20Response.class, respType))
+ .thenCompose(jsonRpc20Response -> {
+ JsonRpc20Response resp = (JsonRpc20Response) jsonRpc20Response;
+ if (resp.getError() != null) {
+ final MevShareApiException e;
+ if (resp.getThrowable() != null) {
+ e = new MevShareApiException(resp.getThrowable());
+ e.setError(resp.getError());
+ } else {
+ e = new MevShareApiException(resp.getError());
+ }
+ future.completeExceptionally(e);
+ } else {
+ future.complete(resp.getResult());
+ }
+ return future;
+ })
+ .exceptionallyCompose(throwable -> {
+ MevShareApiException e = new MevShareApiException(throwable);
+ future.completeExceptionally(e);
+ return future;
+ });
+ }
+
+ /**
+ * Event source factory eventSourceFactory
+ *
+ * @return the eventSourceFactory
+ */
+ public EventSource.Factory eventSourceFactory() {
+ return eventSourceFactory;
+ }
+
+ /**
+ * Create json rpc 20 request createJsonRpc20Request.
+ *
+ * @param method the method
+ * @param params the params
+ * @return the createJsonRpc20Request
+ */
+ public JsonRpc20Request createJsonRpc20Request(String method, List> params) {
+ final JsonRpc20Request request = new JsonRpc20Request();
+ request.setId(nextId());
+ request.setMethod(method);
+ request.setParams(params);
+ return request;
+ }
+
+ private long nextId() {
+ return nextId.incrementAndGet();
+ }
+}
diff --git a/src/main/java/net/flashbots/provider/json/BigIntToHexStringSerializer.java b/src/main/java/net/flashbots/provider/json/BigIntToHexStringSerializer.java
new file mode 100644
index 0000000..0f8c84d
--- /dev/null
+++ b/src/main/java/net/flashbots/provider/json/BigIntToHexStringSerializer.java
@@ -0,0 +1,30 @@
+package net.flashbots.provider.json;
+
+import java.io.IOException;
+import java.math.BigInteger;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import org.web3j.utils.Numeric;
+
+/**
+ * The type BigIntToHexStringSerializer.
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class BigIntToHexStringSerializer extends JsonSerializer {
+
+ /**
+ * Instantiates a new BigIntToHexStringSerializer.
+ */
+ public BigIntToHexStringSerializer() {
+ super();
+ }
+
+ @Override
+ public void serialize(BigInteger value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+ gen.writeString(Numeric.toHexStringWithPrefix(value));
+ }
+}
diff --git a/src/main/java/net/flashbots/provider/json/HexStringToBigIntDeserializer.java b/src/main/java/net/flashbots/provider/json/HexStringToBigIntDeserializer.java
new file mode 100644
index 0000000..14de274
--- /dev/null
+++ b/src/main/java/net/flashbots/provider/json/HexStringToBigIntDeserializer.java
@@ -0,0 +1,27 @@
+package net.flashbots.provider.json;
+
+import java.io.IOException;
+import java.math.BigInteger;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import org.web3j.utils.Numeric;
+
+/**
+ * The type HexStringToBigIntDeserializer.
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class HexStringToBigIntDeserializer extends JsonDeserializer {
+ /**
+ * Instantiates a new HexStringToBigIntDeserializer.
+ */
+ public HexStringToBigIntDeserializer() {}
+
+ @Override
+ public BigInteger deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ return Numeric.toBigInt(p.getValueAsString());
+ }
+}
diff --git a/src/main/java/net/flashbots/provider/json/HintPreferencesSerializer.java b/src/main/java/net/flashbots/provider/json/HintPreferencesSerializer.java
new file mode 100644
index 0000000..d26e98e
--- /dev/null
+++ b/src/main/java/net/flashbots/provider/json/HintPreferencesSerializer.java
@@ -0,0 +1,46 @@
+package net.flashbots.provider.json;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import net.flashbots.models.bundle.HintPreferences;
+
+/**
+ * The type HintPreferencesSerializer.
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+public class HintPreferencesSerializer extends JsonSerializer {
+
+ /**
+ * Instantiates a new HintPreferencesSerializer.
+ */
+ public HintPreferencesSerializer() {
+ super();
+ }
+
+ @Override
+ public void serialize(HintPreferences value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+ gen.writeStartArray();
+ if (value.isCalldata()) {
+ gen.writeString("calldata");
+ }
+ if (value.isContractAddress()) {
+ gen.writeString("contract_address");
+ }
+ if (value.isFunctionSelector()) {
+ gen.writeString("function_selector");
+ }
+ if (value.isLogs()) {
+ gen.writeString("logs");
+ }
+ if (value.isTxHash()) {
+ gen.writeString("tx_hash");
+ }
+ gen.writeString("hash");
+ gen.writeEndArray();
+ }
+}
diff --git a/src/test/java/net/flashbots/MevShareClientTest.java b/src/test/java/net/flashbots/MevShareClientTest.java
new file mode 100644
index 0000000..daa31d2
--- /dev/null
+++ b/src/test/java/net/flashbots/MevShareClientTest.java
@@ -0,0 +1,344 @@
+package net.flashbots;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.reactivex.disposables.Disposable;
+import net.flashbots.models.bundle.*;
+import net.flashbots.models.common.Network;
+import net.flashbots.models.event.EventHistoryEntry;
+import net.flashbots.models.event.EventHistoryInfo;
+import net.flashbots.models.event.EventHistoryParams;
+import net.flashbots.models.event.MevShareEvent;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.web3j.crypto.Credentials;
+import org.web3j.crypto.ECKeyPair;
+import org.web3j.crypto.Keys;
+import org.web3j.crypto.RawTransaction;
+import org.web3j.crypto.TransactionEncoder;
+import org.web3j.protocol.Web3j;
+import org.web3j.protocol.core.DefaultBlockParameter;
+import org.web3j.protocol.core.DefaultBlockParameterName;
+import org.web3j.protocol.core.methods.response.EthBlock;
+import org.web3j.protocol.http.HttpService;
+import org.web3j.tx.gas.DefaultGasProvider;
+import org.web3j.utils.Convert;
+import org.web3j.utils.Numeric;
+
+/**
+ * The type MevShareClientTest.
+ *
+ * @author kaichen
+ * @since 0.1.0
+ */
+class MevShareClientTest {
+
+ private static Credentials AUTH_SIGNER;
+
+ private static Credentials SIGNER;
+
+ private static MevShareClient MEV_SHARE_CLIENT;
+
+ private static Web3j WEB3J;
+
+ @BeforeAll
+ static void beforeAll()
+ throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException {
+ AUTH_SIGNER = Credentials.create(Keys.createEcKeyPair());
+ SIGNER = Credentials.create(System.getenv("SIGNER_PRIVATE_KEY"));
+ MEV_SHARE_CLIENT = new MevShareClient(Network.GOERLI, AUTH_SIGNER, WEB3J);
+ WEB3J = Web3j.build(new HttpService(System.getenv("GOERLI_RPC_URL")));
+ }
+
+ /**
+ * Gets event history info.
+ *
+ * @throws ExecutionException the execution exception
+ * @throws InterruptedException the interrupted exception
+ */
+ @Test
+ @DisplayName("Get event history info")
+ void getEventHistoryInfo() throws ExecutionException, InterruptedException {
+ EventHistoryInfo eventHistoryInfo =
+ MEV_SHARE_CLIENT.getEventHistoryInfo().get();
+ System.out.println(eventHistoryInfo);
+ assertTrue(eventHistoryInfo.getCount().compareTo(BigInteger.ZERO) > 0);
+ assertTrue(eventHistoryInfo.getMinBlock().compareTo(BigInteger.ZERO) > 0);
+ assertTrue(eventHistoryInfo.getMaxBlock().compareTo(BigInteger.ZERO) > 0);
+ assertTrue(eventHistoryInfo.getMinTimestamp().compareTo(BigInteger.ZERO) > 0);
+ assertTrue(eventHistoryInfo.getMaxTimestamp().compareTo(BigInteger.ZERO) > 0);
+ assertTrue(eventHistoryInfo.getMaxLimit().compareTo(BigInteger.ZERO) > 0);
+ }
+
+ @Test
+ @DisplayName("Get event history")
+ void getEventHistory() throws ExecutionException, InterruptedException {
+ List eventHistoryEntries = MEV_SHARE_CLIENT
+ .getEventHistory(new EventHistoryParams().setLimit(20).setBlockStart(BigInteger.valueOf(1000000)))
+ .get();
+ System.out.println(eventHistoryEntries);
+ assertEquals(20, eventHistoryEntries.size());
+ }
+
+ @Test
+ @DisplayName("Subscribe event")
+ void subscribeEvent() throws InterruptedException, ExecutionException {
+ final CountDownLatch latch = new CountDownLatch(3);
+ var ref = new AtomicReference();
+ Disposable disposable = MEV_SHARE_CLIENT.subscribe(mevShareEvent -> {
+ if (mevShareEvent.getLogs() != null) {
+ ref.getAndSet(mevShareEvent);
+ latch.countDown();
+ }
+ });
+ latch.await();
+ disposable.dispose();
+ assertNotNull(ref.get().getHash());
+ }
+
+ @Test
+ @DisplayName("Send bundle with hash")
+ void sendBundle()
+ throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException,
+ ExecutionException, InterruptedException, IOException {
+ CompletableFuture future = new CompletableFuture<>();
+ Disposable disposable = MEV_SHARE_CLIENT.subscribe(mevShareEvent -> {
+ if (mevShareEvent.getHash() != null) {
+ future.complete(mevShareEvent);
+ }
+ });
+ MevShareEvent mevShareEvent = future.get();
+ disposable.dispose();
+
+ BigInteger number = WEB3J.ethGetBlockByNumber(DefaultBlockParameterName.LATEST, false)
+ .send()
+ .getBlock()
+ .getNumber();
+
+ Inclusion inclusion =
+ new Inclusion().setBlock(number.add(BigInteger.ONE)).setMaxBlock(number.add(BigInteger.valueOf(4)));
+
+ BundleItemType.HashItem bundleItem = new BundleItemType.HashItem().setHash(mevShareEvent.getHash());
+
+ ECKeyPair senderKeyPair = Keys.createEcKeyPair();
+ Credentials sender = Credentials.create(senderKeyPair);
+ BigInteger nonce = WEB3J.ethGetTransactionCount(sender.getAddress(), DefaultBlockParameterName.PENDING)
+ .send()
+ .getTransactionCount();
+ BigInteger gasPrice = WEB3J.ethGasPrice().send().getGasPrice();
+ BigInteger gasLimit = DefaultGasProvider.GAS_LIMIT;
+ final String to = "0x56EdF679B0C80D528E17c5Ffe514dc9a1b254b9c";
+ final String amount = "0.01";
+ RawTransaction rawTransaction = RawTransaction.createEtherTransaction(
+ nonce,
+ gasPrice,
+ gasLimit,
+ to,
+ Convert.toWei(amount, Convert.Unit.ETHER).toBigInteger());
+ byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, Network.GOERLI.chainId(), sender);
+ String hexValue = Numeric.toHexString(signedMessage);
+
+ BundleItemType.TxItem bundleItem1 =
+ new BundleItemType.TxItem().setTx(hexValue).setCanRevert(true);
+
+ BundleParams bundleParams =
+ new BundleParams().setInclusion(inclusion).setBody(List.of(bundleItem, bundleItem1));
+
+ CompletableFuture res = MEV_SHARE_CLIENT.sendBundle(bundleParams);
+
+ System.out.println(res.get().getBundleHash());
+ assertNotNull(res.get().getBundleHash());
+ }
+
+ @Test
+ @DisplayName("Send bundle without hash")
+ void sendBundleWithoutHash()
+ throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException,
+ ExecutionException, InterruptedException, IOException {
+
+ BigInteger number = WEB3J.ethGetBlockByNumber(DefaultBlockParameterName.LATEST, false)
+ .send()
+ .getBlock()
+ .getNumber();
+
+ Inclusion inclusion =
+ new Inclusion().setBlock(number.add(BigInteger.ONE)).setMaxBlock(number.add(BigInteger.valueOf(4)));
+
+ ECKeyPair senderKeyPair = Keys.createEcKeyPair();
+ Credentials sender = Credentials.create(senderKeyPair);
+ BigInteger nonce = WEB3J.ethGetTransactionCount(sender.getAddress(), DefaultBlockParameterName.PENDING)
+ .send()
+ .getTransactionCount();
+ BigInteger gasPrice = WEB3J.ethGasPrice().send().getGasPrice();
+ BigInteger gasLimit = DefaultGasProvider.GAS_LIMIT;
+ final String to = "0x56EdF679B0C80D528E17c5Ffe514dc9a1b254b9c";
+ final String amount = "0.01";
+ RawTransaction rawTransaction = RawTransaction.createEtherTransaction(
+ nonce,
+ gasPrice,
+ gasLimit,
+ to,
+ Convert.toWei(amount, Convert.Unit.ETHER).toBigInteger());
+ byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, Network.GOERLI.chainId(), sender);
+ String hexValue = Numeric.toHexString(signedMessage);
+
+ BundleItemType.TxItem bundleItem =
+ new BundleItemType.TxItem().setTx(hexValue).setCanRevert(true);
+
+ HintPreferences hintPreferences = new HintPreferences()
+ .setCalldata(true)
+ .setContractAddress(true)
+ .setFunctionSelector(true)
+ .setLogs(true)
+ .setTxHash(true);
+ List builders = new ArrayList<>();
+ builders.add("flashbots");
+ BundlePrivacy bundlePrivacy =
+ new BundlePrivacy().setHints(hintPreferences).setBuilders(builders);
+
+ BundleParams bundleParams = new BundleParams()
+ .setInclusion(inclusion)
+ .setBody(List.of(bundleItem))
+ .setPrivacy(bundlePrivacy);
+
+ CompletableFuture res = MEV_SHARE_CLIENT.sendBundle(bundleParams);
+
+ System.out.println(res.get().getBundleHash());
+ assertNotNull(res.get().getBundleHash());
+ }
+
+ @Test
+ @DisplayName("Simulate bundle")
+ void simBundle() throws ExecutionException, InterruptedException, IOException {
+ var latestBlock = WEB3J.ethGetBlockByNumber(DefaultBlockParameterName.LATEST, false)
+ .send()
+ .getBlock();
+ var parentBlock = WEB3J.ethGetBlockByNumber(
+ DefaultBlockParameter.valueOf(latestBlock.getNumber().subtract(BigInteger.ONE)), false)
+ .send()
+ .getBlock();
+
+ Inclusion inclusion = new Inclusion()
+ .setBlock(latestBlock.getNumber().subtract(BigInteger.ONE))
+ .setMaxBlock(latestBlock.getNumber().add(BigInteger.valueOf(10)));
+
+ BigInteger nonce = WEB3J.ethGetTransactionCount(SIGNER.getAddress(), DefaultBlockParameterName.PENDING)
+ .send()
+ .getTransactionCount();
+ final String to = "0x56EdF679B0C80D528E17c5Ffe514dc9a1b254b9c";
+ RawTransaction rawTransaction = RawTransaction.createEtherTransaction(
+ nonce,
+ WEB3J.ethGasPrice().send().getGasPrice(),
+ DefaultGasProvider.GAS_LIMIT,
+ to,
+ Convert.toWei("0", Convert.Unit.ETHER).toBigInteger());
+ byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, Network.GOERLI.chainId(), SIGNER);
+ String hexValue = Numeric.toHexString(signedMessage);
+
+ BundleItemType.TxItem bundleItem =
+ new BundleItemType.TxItem().setTx(hexValue).setCanRevert(true);
+
+ BundleParams bundleParams = new BundleParams().setInclusion(inclusion).setBody(List.of(bundleItem));
+ SimBundleOptions options = new SimBundleOptions();
+
+ options.setParentBlock(latestBlock.getNumber().subtract(BigInteger.ONE));
+ options.setBlockNumber(latestBlock.getNumber());
+ options.setTimestamp(parentBlock.getTimestamp().add(BigInteger.valueOf(12)));
+ options.setGasLimit(parentBlock.getGasLimit());
+ options.setBaseFee(parentBlock.getBaseFeePerGas());
+ options.setTimeout(30);
+
+ var res = MEV_SHARE_CLIENT.simBundle(bundleParams, options);
+
+ System.out.println(res.get().toString());
+ assertTrue(res.get().getSuccess());
+ }
+
+ @Test
+ @DisplayName("Send private transaction")
+ void sendPrivateTransaction()
+ throws IOException, InterruptedException, ExecutionException, InvalidAlgorithmParameterException,
+ NoSuchAlgorithmException, NoSuchProviderException {
+ EthBlock.Block latest = WEB3J.ethGetBlockByNumber(DefaultBlockParameterName.LATEST, false)
+ .send()
+ .getBlock();
+
+ BigInteger maxPriorityFeePerGas = BigInteger.valueOf(1_000_000_000L);
+
+ Credentials sender = Credentials.create(Keys.createEcKeyPair());
+ BigInteger nonce = WEB3J.ethGetTransactionCount(sender.getAddress(), DefaultBlockParameterName.PENDING)
+ .send()
+ .getTransactionCount();
+ final String to = "0x56EdF679B0C80D528E17c5Ffe514dc9a1b254b9c";
+
+ RawTransaction rawTransaction = RawTransaction.createTransaction(
+ 5L,
+ nonce,
+ latest.getGasLimit(),
+ to,
+ Convert.toWei("0", Convert.Unit.ETHER).toBigInteger(),
+ Numeric.toHexString("im shariiiiiing".getBytes(StandardCharsets.UTF_8)),
+ maxPriorityFeePerGas,
+ latest.getBaseFeePerGas().multiply(BigInteger.TWO).add(maxPriorityFeePerGas));
+ byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, Network.GOERLI.chainId(), sender);
+ String signRawTx = Numeric.toHexString(signedMessage);
+
+ PrivateTxOptions txOptions = new PrivateTxOptions()
+ .setHints(new HintPreferences()
+ .setCalldata(true)
+ .setContractAddress(true)
+ .setFunctionSelector(true)
+ .setLogs(true));
+
+ CompletableFuture res = MEV_SHARE_CLIENT.sendPrivateTransaction(signRawTx, txOptions);
+ System.out.println(res.get());
+ assertNotNull(res.get());
+ }
+
+ @Test
+ @DisplayName("Subscribe tx event")
+ void subscribeTx() throws ExecutionException, InterruptedException {
+ CompletableFuture future = new CompletableFuture<>();
+ Disposable disposable = MEV_SHARE_CLIENT.subscribeTx(mevShareEvent -> {
+ if (mevShareEvent.getLogs() != null) {
+ future.complete(mevShareEvent);
+ }
+ });
+ MevShareEvent mevShareEvent = future.get();
+ disposable.dispose();
+ assertTrue(mevShareEvent.getTxs() == null || mevShareEvent.getTxs().size() == 1);
+ }
+
+ @Disabled("No bundle event in goerli")
+ @Test
+ @DisplayName("Subscribe bundle event")
+ void subscribeBundle() throws ExecutionException, InterruptedException {
+ CompletableFuture future = new CompletableFuture<>();
+ Disposable disposable = MEV_SHARE_CLIENT.subscribeBundle(mevShareEvent -> {
+ if (mevShareEvent.getLogs() != null) {
+ future.complete(mevShareEvent);
+ }
+ });
+
+ MevShareEvent mevShareEvent = future.get();
+ disposable.dispose();
+ assertTrue(mevShareEvent.getTxs().size() > 1);
+ }
+}
diff --git a/src/test/resources/log4j2.xml b/src/test/resources/log4j2.xml
new file mode 100644
index 0000000..f2d5a09
--- /dev/null
+++ b/src/test/resources/log4j2.xml
@@ -0,0 +1,16 @@
+
+
+
+ %d{yyyy-MM-dd'T'HH:mm:ss.SSSZ} %p %m%n
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file