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 @@ +branches30.3% \ 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 @@ +coverage62.7% \ 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