diff --git a/.github/workflows/check-pr.yml b/.github/workflows/check-pr.yml
index 3c3b2246..87dd0d39 100644
--- a/.github/workflows/check-pr.yml
+++ b/.github/workflows/check-pr.yml
@@ -6,6 +6,18 @@ jobs:
check-pr:
name: Check PR
runs-on: ubuntu-latest
+ env:
+ JAVA_OPTS: -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8
+ BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
+ SBT_IT_TEST_THREADS: 2
+ services:
+ docker:
+ image: docker:latest
+ options: --privileged # Required for Docker-in-Docker
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ ports:
+ - 2375:2375
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
@@ -13,5 +25,15 @@ jobs:
distribution: 'temurin'
java-version: '11'
cache: 'sbt'
- - name: Check PR
- run: sbt --batch "compile;test"
+ - uses: sbt/setup-sbt@v1
+ - name: Run tests
+ run: |
+ sbt --batch "test;docker;consensus-client-it/test"
+ - name: Archive logs
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: test-logs_${{ env.BRANCH_NAME }}_${{ github.run_id }}
+ path: consensus-client-it/target/test-logs
+ if-no-files-found: warn
+ retention-days: 14
diff --git a/.gitignore b/.gitignore
index 80467331..fcbe3df0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,9 @@
.DS_Store
target
+.bloop
.bsp
+.metals
+metals.sbt
.idea
docker/data
docker/logs
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..528a10d0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,20 @@
+# consensus-client
+
+## 👨💻 Development
+
+1. Run unit tests:
+ ```bash
+ sbt test
+ ```
+
+2. Run integration tests:
+ 1. Build the Docker image:
+ ```bash
+ sbt docker
+ ```
+ Note: Build the Docker image whenever the consensus client code is updated, including after pulling from the repository.
+ 2. Run the integration tests:
+ ```bash
+ sbt "consensus-client-it/test"
+ ```
+ 3. See logs in `consensus-client-it/target/test-logs`.
diff --git a/build.sbt b/build.sbt
index e8f12f33..1060985f 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,37 +1,43 @@
-Global / onChangedBuildSource := ReloadOnSourceChanges
+import com.github.sbt.git.SbtGit.GitKeys.gitCurrentBranch
-enablePlugins(UniversalDeployPlugin, GitVersioning)
+enablePlugins(UniversalDeployPlugin, GitVersioning, sbtdocker.DockerPlugin)
git.useGitDescribe := true
git.baseVersion := "1.0.0"
git.uncommittedSignifier := Some("DIRTY")
-scalaVersion := "2.13.15"
-organization := "network.units"
-organizationName := "Units Network"
-name := "consensus-client"
-maintainer := "Units Network Team"
-resolvers ++= Resolver.sonatypeOssRepos("releases") ++ Resolver.sonatypeOssRepos("snapshots") ++ Seq(Resolver.mavenLocal)
+inScope(Global)(
+ Seq(
+ onChangedBuildSource := ReloadOnSourceChanges,
+ scalaVersion := "2.13.15",
+ organization := "network.units",
+ organizationName := "Units Network",
+ resolvers ++= Resolver.sonatypeOssRepos("releases") ++ Resolver.sonatypeOssRepos("snapshots") ++ Seq(Resolver.mavenLocal),
+ scalacOptions ++= Seq(
+ "-Xsource:3",
+ "-feature",
+ "-deprecation",
+ "-unchecked",
+ "-language:higherKinds",
+ "-language:implicitConversions",
+ "-language:postfixOps",
+ "-Ywarn-unused:-implicits",
+ "-Xlint"
+ )
+ )
+)
+
+name := "consensus-client"
+maintainer := "Units Network Team"
+
libraryDependencies ++= Seq(
- "com.wavesplatform" % "node-testkit" % "1.5.8" % "test",
- "com.wavesplatform" % "node" % "1.5.8" % "provided",
+ "com.wavesplatform" % "node-testkit" % "1.5.8" % Test,
+ "com.wavesplatform" % "node" % "1.5.8" % Provided,
"com.softwaremill.sttp.client3" % "core_2.13" % "3.10.1",
"com.softwaremill.sttp.client3" %% "play-json" % "3.10.1",
"com.github.jwt-scala" %% "jwt-play-json" % "10.0.1"
)
-scalacOptions ++= Seq(
- "-Xsource:3",
- "-feature",
- "-deprecation",
- "-unchecked",
- "-language:higherKinds",
- "-language:implicitConversions",
- "-language:postfixOps",
- "-Ywarn-unused:-implicits",
- "-Xlint"
-)
-
Compile / packageDoc / publishArtifact := false
def makeJarName(
@@ -81,3 +87,23 @@ buildTarballsForDocker := {
baseDirectory.value / "docker" / "target" / "consensus-client.tgz"
)
}
+
+inTask(docker)(
+ Seq(
+ imageNames := Seq(
+ ImageName(s"consensus-client:${gitCurrentBranch.value}"), // Integration tests
+ ImageName("consensus-client:local") // local-network
+ ),
+ dockerfile := NativeDockerfile(baseDirectory.value / "docker" / "Dockerfile"),
+ buildOptions := BuildOptions(cache = true)
+ )
+)
+
+docker := docker.dependsOn(LocalRootProject / buildTarballsForDocker).value
+
+lazy val `consensus-client` = project.in(file("."))
+
+lazy val `consensus-client-it` = project
+ .dependsOn(
+ `consensus-client` % "compile;test->test"
+ )
diff --git a/consensus-client-it/build.sbt b/consensus-client-it/build.sbt
new file mode 100644
index 00000000..ed32665e
--- /dev/null
+++ b/consensus-client-it/build.sbt
@@ -0,0 +1,67 @@
+import com.github.sbt.git.SbtGit.git.gitCurrentBranch
+import sbt.Tests.Group
+
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+
+description := "Consensus client integration tests"
+
+libraryDependencies ++= Seq(
+ "org.testcontainers" % "testcontainers" % "1.20.3",
+ "org.web3j" % "core" % "4.9.8"
+).map(_ % Test)
+
+val logsDirectory = taskKey[File]("The directory for logs") // Task to evaluate and recreate the logs directory every time
+
+Global / concurrentRestrictions := {
+ val threadNumber = Option(System.getenv("SBT_IT_TEST_THREADS")).fold(1)(_.toInt)
+ Seq(Tags.limit(Tags.ForkedTestGroup, threadNumber))
+}
+
+inConfig(Test)(
+ Seq(
+ logsDirectory := {
+ val runId: String = Option(System.getenv("RUN_ID")).getOrElse(DateTimeFormatter.ofPattern("MM-dd--HH_mm_ss").format(LocalDateTime.now))
+ val r = target.value / "test-logs" / runId
+ r.mkdirs()
+ r
+ },
+ javaOptions ++= Seq(
+ s"-Dlogback.configurationFile=${(Test / resourceDirectory).value}/logback-test.xml", // Fixes a logback blaming for multiple configs
+ s"-Dcc.it.configs.dir=${baseDirectory.value.getParent}/local-network/configs",
+ s"-Dcc.it.docker.image=consensus-client:${gitCurrentBranch.value}"
+ ),
+ testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-fFWD", ((Test / logsDirectory).value / "summary.log").toString),
+ fork := true,
+ testForkedParallel := true,
+ testGrouping := {
+ val javaHomeVal = (test / javaHome).value
+ val baseLogDirVal = (Test / logsDirectory).value
+ val envVarsVal = (Test / envVars).value
+ val javaOptionsVal = (Test / javaOptions).value
+
+ val tests = (Test / definedTests).value
+
+ tests.zipWithIndex.map { case (suite, i) =>
+ val suiteLogDir = baseLogDirVal / suite.name.replaceAll("""(\w)\w*\.""", "$1.") // foo.bar.Baz -> f.b.Baz
+ Group(
+ suite.name,
+ Seq(suite),
+ Tests.SubProcess(
+ ForkOptions(
+ javaHome = javaHomeVal,
+ outputStrategy = (Test / outputStrategy).value,
+ bootJars = Vector.empty[java.io.File],
+ workingDirectory = Option((Test / baseDirectory).value),
+ runJVMOptions = Vector(
+ s"-Dcc.it.logs.dir=$suiteLogDir"
+ ) ++ javaOptionsVal,
+ connectInput = false,
+ envVars = envVarsVal
+ )
+ )
+ )
+ }
+ }
+ )
+)
diff --git a/consensus-client-it/src/test/java/units/bridge/BridgeContract.java b/consensus-client-it/src/test/java/units/bridge/BridgeContract.java
new file mode 100644
index 00000000..f4944b65
--- /dev/null
+++ b/consensus-client-it/src/test/java/units/bridge/BridgeContract.java
@@ -0,0 +1,286 @@
+package units.bridge;
+
+import io.reactivex.Flowable;
+import org.web3j.abi.EventEncoder;
+import org.web3j.abi.TypeReference;
+import org.web3j.abi.datatypes.Address;
+import org.web3j.abi.datatypes.Event;
+import org.web3j.abi.datatypes.Function;
+import org.web3j.abi.datatypes.Type;
+import org.web3j.abi.datatypes.generated.Bytes20;
+import org.web3j.abi.datatypes.generated.Uint256;
+import org.web3j.abi.datatypes.primitive.Int;
+import org.web3j.abi.datatypes.primitive.Long;
+import org.web3j.crypto.Credentials;
+import org.web3j.protocol.Web3j;
+import org.web3j.protocol.core.DefaultBlockParameter;
+import org.web3j.protocol.core.RemoteCall;
+import org.web3j.protocol.core.RemoteFunctionCall;
+import org.web3j.protocol.core.methods.request.EthFilter;
+import org.web3j.protocol.core.methods.response.BaseEventResponse;
+import org.web3j.protocol.core.methods.response.Log;
+import org.web3j.protocol.core.methods.response.TransactionReceipt;
+import org.web3j.tx.Contract;
+import org.web3j.tx.TransactionManager;
+import org.web3j.tx.gas.ContractGasProvider;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ *
Auto generated code.
+ *
Do not modify!
+ *
Please use the web3j command line tools,
+ * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the
+ * codegen module to update.
+ *
+ *
Generated with web3j version 1.6.1.
+ */
+@SuppressWarnings("rawtypes")
+public class BridgeContract extends Contract {
+ public static final String BINARY = "6080604052348015600e575f80fd5b506108b68061001c5f395ff3fe60806040526004361061006e575f3560e01c806396f396c31161004c57806396f396c3146100e3578063c4a4326d14610105578063e984df0e1461011d578063fccc281314610131575f80fd5b806339dd5d1b146100725780637157405a146100b957806378338413146100ce575b5f80fd5b34801561007d575f80fd5b506100a161008c36600461059e565b5f6020819052908152604090205461ffff1681565b60405161ffff90911681526020015b60405180910390f35b3480156100c4575f80fd5b506100a161040081565b6100e16100dc3660046105b5565b61015c565b005b3480156100ee575f80fd5b506100f761044e565b6040519081526020016100b0565b348015610110575f80fd5b506100f76402540be40081565b348015610128575f80fd5b506100f7610468565b34801561013c575f80fd5b506101445f81565b6040516001600160a01b0390911681526020016100b0565b61016c6402540be40060016105fc565b34101561017834610478565b61019061018b6402540be40060016105fc565b610478565b6040516020016101a1929190610630565b604051602081830303815290604052906101d75760405162461bcd60e51b81526004016101ce9190610688565b60405180910390fd5b506101ef6402540be400677fffffffffffffff6105fc565b3411156101fb34610478565b61021561018b6402540be400677fffffffffffffff6105fc565b6040516020016102269291906106bd565b604051602081830303815290604052906102535760405162461bcd60e51b81526004016101ce9190610688565b50435f8181526020819052604090205461ffff166104009081119061027790610478565b604051602001610287919061070c565b604051602081830303815290604052906102b45760405162461bcd60e51b81526004016101ce9190610688565b505f818152602081905260408120805461ffff16916102d283610786565b91906101000a81548161ffff021916908361ffff160217905550505f6402540be400346102ff91906107a6565b9050346103116402540be400836105fc565b1461031b34610478565b6103296402540be400610478565b60405160200161033a9291906107c5565b604051602081830303815290604052906103675760405162461bcd60e51b81526004016101ce9190610688565b506040515f90819034908281818185825af1925050503d805f81146103a7576040519150601f19603f3d011682016040523d82523d5f602084013e6103ac565b606091505b50509050806103fd5760405162461bcd60e51b815260206004820152601e60248201527f4661696c656420746f2073656e6420746f206275726e2061646472657373000060448201526064016101ce565b604080516bffffffffffffffffffffffff1986168152600784900b60208201527ffeadaf04de8d7c2594453835b9a93b747e20e7a09a7fdb9280579a6dbaf131a8910160405180910390a150505050565b6104656402540be400677fffffffffffffff6105fc565b81565b6104656402540be40060016105fc565b6060815f0361049e5750506040805180820190915260018152600360fc1b602082015290565b815f5b81156104c757806104b181610814565b91506104c09050600a836107a6565b91506104a1565b5f8167ffffffffffffffff8111156104e1576104e161082c565b6040519080825280601f01601f19166020018201604052801561050b576020820181803683370190505b509050815b851561059557610521600182610840565b90505f61052f600a886107a6565b61053a90600a6105fc565b6105449088610840565b61054f906030610853565b90505f8160f81b90508084848151811061056b5761056b61086c565b60200101906001600160f81b03191690815f1a90535061058c600a896107a6565b97505050610510565b50949350505050565b5f602082840312156105ae575f80fd5b5035919050565b5f602082840312156105c5575f80fd5b81356bffffffffffffffffffffffff19811681146105e1575f80fd5b9392505050565b634e487b7160e01b5f52601160045260245ffd5b8082028115828204841417610613576106136105e8565b92915050565b5f81518060208401855e5f93019283525090919050565b6a029b2b73a103b30b63ab2960ad1b81525f61064f600b830185610619565b7f206d7573742062652067726561746572206f7220657175616c20746f20000000815261067f601d820185610619565b95945050505050565b602081525f82518060208401528060208501604085015e5f604082850101526040601f19601f83011684010191505092915050565b6a029b2b73a103b30b63ab2960ad1b81525f6106dc600b830185610619565b7f206d757374206265206c657373206f7220657175616c20746f20000000000000815261067f601a820185610619565b7f4d6178207472616e7366657273206c696d6974206f662000000000000000000081525f61073d6017830184610619565b7f207265616368656420696e207468697320626c6f636b2e2054727920746f207381527232b732103a3930b739b332b9399030b3b0b4b760691b60208201526033019392505050565b5f61ffff821661ffff810361079d5761079d6105e8565b60010192915050565b5f826107c057634e487b7160e01b5f52601260045260245ffd5b500490565b6a029b2b73a103b30b63ab2960ad1b81525f6107e4600b830185610619565b7f206d7573742062652061206d756c7469706c65206f6620000000000000000000815261067f6017820185610619565b5f60018201610825576108256105e8565b5060010190565b634e487b7160e01b5f52604160045260245ffd5b81810381811115610613576106136105e8565b60ff8181168382160190811115610613576106136105e8565b634e487b7160e01b5f52603260045260245ffdfea2646970667358221220106399f534da089226c14e2f183f8421d059a924c65c97d7e4f3e931c54fe1bb64736f6c634300081a0033";
+
+ private static String librariesLinkedBinary;
+
+ public static final String FUNC_BURN_ADDRESS = "BURN_ADDRESS";
+
+ public static final String FUNC_EL_TO_CL_RATIO = "EL_TO_CL_RATIO";
+
+ public static final String FUNC_MAX_AMOUNT_IN_WEI = "MAX_AMOUNT_IN_WEI";
+
+ public static final String FUNC_MAX_TRANSFERS_IN_BLOCK = "MAX_TRANSFERS_IN_BLOCK";
+
+ public static final String FUNC_MIN_AMOUNT_IN_WEI = "MIN_AMOUNT_IN_WEI";
+
+ public static final String FUNC_SENDNATIVE = "sendNative";
+
+ public static final String FUNC_TRANSFERSPERBLOCK = "transfersPerBlock";
+
+ public static final Event SENTNATIVE_EVENT = new Event("SentNative",
+ Arrays.>asList(new TypeReference() {
+ }, new TypeReference() {
+ }));
+ ;
+
+ @Deprecated
+ protected BridgeContract(String contractAddress, Web3j web3j, Credentials credentials,
+ BigInteger gasPrice, BigInteger gasLimit) {
+ super(BINARY, contractAddress, web3j, credentials, gasPrice, gasLimit);
+ }
+
+ protected BridgeContract(String contractAddress, Web3j web3j, Credentials credentials,
+ ContractGasProvider contractGasProvider) {
+ super(BINARY, contractAddress, web3j, credentials, contractGasProvider);
+ }
+
+ @Deprecated
+ protected BridgeContract(String contractAddress, Web3j web3j,
+ TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) {
+ super(BINARY, contractAddress, web3j, transactionManager, gasPrice, gasLimit);
+ }
+
+ protected BridgeContract(String contractAddress, Web3j web3j,
+ TransactionManager transactionManager, ContractGasProvider contractGasProvider) {
+ super(BINARY, contractAddress, web3j, transactionManager, contractGasProvider);
+ }
+
+ public static List getSentNativeEvents(
+ TransactionReceipt transactionReceipt) {
+ List valueList = staticExtractEventParametersWithLog(SENTNATIVE_EVENT, transactionReceipt);
+ ArrayList responses = new ArrayList(valueList.size());
+ for (Contract.EventValuesWithLog eventValues : valueList) {
+ SentNativeEventResponse typedResponse = new SentNativeEventResponse();
+ typedResponse.log = eventValues.getLog();
+ typedResponse.wavesRecipient = (byte[]) eventValues.getNonIndexedValues().get(0).getValue();
+ typedResponse.amount = (java.lang.Long) eventValues.getNonIndexedValues().get(1).getValue();
+ responses.add(typedResponse);
+ }
+ return responses;
+ }
+
+ public static SentNativeEventResponse getSentNativeEventFromLog(Log log) {
+ Contract.EventValuesWithLog eventValues = staticExtractEventParametersWithLog(SENTNATIVE_EVENT, log);
+ SentNativeEventResponse typedResponse = new SentNativeEventResponse();
+ typedResponse.log = log;
+ typedResponse.wavesRecipient = (byte[]) eventValues.getNonIndexedValues().get(0).getValue();
+ typedResponse.amount = (java.lang.Long) eventValues.getNonIndexedValues().get(1).getValue();
+ return typedResponse;
+ }
+
+ public Flowable sentNativeEventFlowable(EthFilter filter) {
+ return web3j.ethLogFlowable(filter).map(log -> getSentNativeEventFromLog(log));
+ }
+
+ public Flowable sentNativeEventFlowable(
+ DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) {
+ EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress());
+ filter.addSingleTopic(EventEncoder.encode(SENTNATIVE_EVENT));
+ return sentNativeEventFlowable(filter);
+ }
+
+ public RemoteFunctionCall call_BURN_ADDRESS() {
+ final Function function = new Function(FUNC_BURN_ADDRESS,
+ Arrays.asList(),
+ Arrays.>asList(new TypeReference() {
+ }));
+ return executeRemoteCallSingleValueReturn(function, String.class);
+ }
+
+ public RemoteFunctionCall send_BURN_ADDRESS() {
+ final Function function = new Function(
+ FUNC_BURN_ADDRESS,
+ Arrays.asList(),
+ Collections.>emptyList());
+ return executeRemoteCallTransaction(function);
+ }
+
+ public RemoteFunctionCall call_EL_TO_CL_RATIO() {
+ final Function function = new Function(FUNC_EL_TO_CL_RATIO,
+ Arrays.asList(),
+ Arrays.>asList(new TypeReference() {
+ }));
+ return executeRemoteCallSingleValueReturn(function, BigInteger.class);
+ }
+
+ public RemoteFunctionCall send_EL_TO_CL_RATIO() {
+ final Function function = new Function(
+ FUNC_EL_TO_CL_RATIO,
+ Arrays.asList(),
+ Collections.>emptyList());
+ return executeRemoteCallTransaction(function);
+ }
+
+ public RemoteFunctionCall call_MAX_AMOUNT_IN_WEI() {
+ final Function function = new Function(FUNC_MAX_AMOUNT_IN_WEI,
+ Arrays.asList(),
+ Arrays.>asList(new TypeReference() {
+ }));
+ return executeRemoteCallSingleValueReturn(function, BigInteger.class);
+ }
+
+ public RemoteFunctionCall send_MAX_AMOUNT_IN_WEI() {
+ final Function function = new Function(
+ FUNC_MAX_AMOUNT_IN_WEI,
+ Arrays.asList(),
+ Collections.>emptyList());
+ return executeRemoteCallTransaction(function);
+ }
+
+ public RemoteFunctionCall call_MAX_TRANSFERS_IN_BLOCK() {
+ final Function function = new Function(FUNC_MAX_TRANSFERS_IN_BLOCK,
+ Arrays.asList(),
+ Arrays.>asList(new TypeReference() {
+ }));
+ return executeRemoteCallSingleValueReturn(function, Integer.class);
+ }
+
+ public RemoteFunctionCall send_MAX_TRANSFERS_IN_BLOCK() {
+ final Function function = new Function(
+ FUNC_MAX_TRANSFERS_IN_BLOCK,
+ Arrays.asList(),
+ Collections.>emptyList());
+ return executeRemoteCallTransaction(function);
+ }
+
+ public RemoteFunctionCall call_MIN_AMOUNT_IN_WEI() {
+ final Function function = new Function(FUNC_MIN_AMOUNT_IN_WEI,
+ Arrays.asList(),
+ Arrays.>asList(new TypeReference() {
+ }));
+ return executeRemoteCallSingleValueReturn(function, BigInteger.class);
+ }
+
+ public RemoteFunctionCall send_MIN_AMOUNT_IN_WEI() {
+ final Function function = new Function(
+ FUNC_MIN_AMOUNT_IN_WEI,
+ Arrays.asList(),
+ Collections.>emptyList());
+ return executeRemoteCallTransaction(function);
+ }
+
+ public RemoteFunctionCall send_sendNative(byte[] wavesRecipient,
+ BigInteger weiValue) {
+ final Function function = new Function(
+ FUNC_SENDNATIVE,
+ Arrays.asList(new org.web3j.abi.datatypes.generated.Bytes20(wavesRecipient)),
+ Collections.>emptyList());
+ return executeRemoteCallTransaction(function, weiValue);
+ }
+
+ public RemoteFunctionCall call_transfersPerBlock(BigInteger param0) {
+ final Function function = new Function(FUNC_TRANSFERSPERBLOCK,
+ Arrays.asList(new org.web3j.abi.datatypes.generated.Uint256(param0)),
+ Arrays.>asList(new TypeReference() {
+ }));
+ return executeRemoteCallSingleValueReturn(function, Integer.class);
+ }
+
+ public RemoteFunctionCall send_transfersPerBlock(BigInteger param0) {
+ final Function function = new Function(
+ FUNC_TRANSFERSPERBLOCK,
+ Arrays.asList(new org.web3j.abi.datatypes.generated.Uint256(param0)),
+ Collections.>emptyList());
+ return executeRemoteCallTransaction(function);
+ }
+
+ @Deprecated
+ public static BridgeContract load(String contractAddress, Web3j web3j, Credentials credentials,
+ BigInteger gasPrice, BigInteger gasLimit) {
+ return new BridgeContract(contractAddress, web3j, credentials, gasPrice, gasLimit);
+ }
+
+ @Deprecated
+ public static BridgeContract load(String contractAddress, Web3j web3j,
+ TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) {
+ return new BridgeContract(contractAddress, web3j, transactionManager, gasPrice, gasLimit);
+ }
+
+ public static BridgeContract load(String contractAddress, Web3j web3j, Credentials credentials,
+ ContractGasProvider contractGasProvider) {
+ return new BridgeContract(contractAddress, web3j, credentials, contractGasProvider);
+ }
+
+ public static BridgeContract load(String contractAddress, Web3j web3j,
+ TransactionManager transactionManager, ContractGasProvider contractGasProvider) {
+ return new BridgeContract(contractAddress, web3j, transactionManager, contractGasProvider);
+ }
+
+ public static RemoteCall deploy(Web3j web3j, Credentials credentials,
+ ContractGasProvider contractGasProvider) {
+ return deployRemoteCall(BridgeContract.class, web3j, credentials, contractGasProvider, getDeploymentBinary(), "");
+ }
+
+ @Deprecated
+ public static RemoteCall deploy(Web3j web3j, Credentials credentials,
+ BigInteger gasPrice, BigInteger gasLimit) {
+ return deployRemoteCall(BridgeContract.class, web3j, credentials, gasPrice, gasLimit, getDeploymentBinary(), "");
+ }
+
+ public static RemoteCall deploy(Web3j web3j,
+ TransactionManager transactionManager, ContractGasProvider contractGasProvider) {
+ return deployRemoteCall(BridgeContract.class, web3j, transactionManager, contractGasProvider, getDeploymentBinary(), "");
+ }
+
+ @Deprecated
+ public static RemoteCall deploy(Web3j web3j,
+ TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) {
+ return deployRemoteCall(BridgeContract.class, web3j, transactionManager, gasPrice, gasLimit, getDeploymentBinary(), "");
+ }
+
+ private static String getDeploymentBinary() {
+ if (librariesLinkedBinary != null) {
+ return librariesLinkedBinary;
+ } else {
+ return BINARY;
+ }
+ }
+
+ public static class SentNativeEventResponse extends BaseEventResponse {
+ public byte[] wavesRecipient;
+
+ public java.lang.Long amount;
+ }
+}
diff --git a/consensus-client-it/src/test/resources/logback-test.xml b/consensus-client-it/src/test/resources/logback-test.xml
new file mode 100644
index 00000000..fc8fb696
--- /dev/null
+++ b/consensus-client-it/src/test/resources/logback-test.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+ DEBUG
+
+
+ ${pattern}
+
+
+
+
+
+ TRACE
+
+ ${cc.it.logs.dir:-target/logs}/test.log
+ false
+
+ ${pattern}
+
+
+
+
+
+ TRACE
+
+ ${cc.it.logs.dir:-target/logs}/test-http.log
+ false
+
+ ${pattern}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/consensus-client-it/src/test/scala/com/wavesplatform/api/LoggingBackend.scala b/consensus-client-it/src/test/scala/com/wavesplatform/api/LoggingBackend.scala
new file mode 100644
index 00000000..0d41bccf
--- /dev/null
+++ b/consensus-client-it/src/test/scala/com/wavesplatform/api/LoggingBackend.scala
@@ -0,0 +1,49 @@
+package com.wavesplatform.api
+
+import com.wavesplatform.api.LoggingBackend.{LoggingOptions, LoggingOptionsTag}
+import com.wavesplatform.utils.ScorexLogging
+import sttp.capabilities.Effect
+import sttp.client3.*
+
+class LoggingBackend[F[_], P](delegate: SttpBackend[F, P]) extends DelegateSttpBackend[F, P](delegate) with ScorexLogging {
+ override def send[T, R >: P & Effect[F]](request: Request[T, R]): F[Response[T]] = {
+ val l = request.tag(LoggingOptionsTag).collect { case l: LoggingOptions => l }
+
+ l.filter(_.logRequest).foreach { l =>
+ var logStr = s"${l.prefix} ${request.method} ${request.uri}"
+ if (l.logRequestBody) logStr += s": body=${request.body.show}"
+ log.debug(logStr)
+ }
+
+ val requestWithRawJson = request.response(asBothOption(request.response, asStringAlways))
+ val withErrorLog = responseMonad.handleError(requestWithRawJson.send(delegate)) { x =>
+ l.foreach { l => log.debug(s"${l.prefix} Error: ${x.getMessage}") }
+ responseMonad.error(x)
+ }
+
+ responseMonad.flatMap(withErrorLog) { response =>
+ l.foreach { l =>
+ var logStr = s"${l.prefix} HTTP ${response.code}"
+ if (l.logResponseBody) logStr += s": body=${response.body._2}"
+ log.debug(logStr)
+ }
+
+ responseMonad.unit(response.copy(body = response.body._1))
+ }
+ }
+}
+
+object LoggingBackend {
+ val LoggingOptionsTag = "logging"
+
+ case class LoggingOptions(
+ logCall: Boolean = true,
+ logResult: Boolean = true,
+ logRequest: Boolean = true,
+ logRequestBody: Boolean = true,
+ logResponseBody: Boolean = true,
+ requestId: Int = LoggingUtil.currRequestId
+ ) {
+ val prefix = s"[$requestId]"
+ }
+}
diff --git a/consensus-client-it/src/test/scala/com/wavesplatform/api/LoggingUtil.scala b/consensus-client-it/src/test/scala/com/wavesplatform/api/LoggingUtil.scala
new file mode 100644
index 00000000..f0f69972
--- /dev/null
+++ b/consensus-client-it/src/test/scala/com/wavesplatform/api/LoggingUtil.scala
@@ -0,0 +1,7 @@
+package com.wavesplatform.api
+
+import java.util.concurrent.ThreadLocalRandom
+
+object LoggingUtil {
+ def currRequestId: Int = ThreadLocalRandom.current().nextInt(10000, 100000)
+}
diff --git a/consensus-client-it/src/test/scala/com/wavesplatform/api/NodeHttpApi.scala b/consensus-client-it/src/test/scala/com/wavesplatform/api/NodeHttpApi.scala
new file mode 100644
index 00000000..bbae09a6
--- /dev/null
+++ b/consensus-client-it/src/test/scala/com/wavesplatform/api/NodeHttpApi.scala
@@ -0,0 +1,290 @@
+package com.wavesplatform.api
+
+import cats.syntax.either.*
+import cats.syntax.option.*
+import com.wavesplatform.account.Address
+import com.wavesplatform.api.LoggingBackend.{LoggingOptions, LoggingOptionsTag}
+import com.wavesplatform.api.NodeHttpApi.*
+import com.wavesplatform.api.http.ApiMarshallers.TransactionJsonWrites
+import com.wavesplatform.api.http.TransactionsApiRoute.ApplicationStatus
+import com.wavesplatform.api.http.`X-Api-Key`
+import com.wavesplatform.common.state.ByteStr
+import com.wavesplatform.state.DataEntry.Format
+import com.wavesplatform.state.{DataEntry, EmptyDataEntry, Height}
+import com.wavesplatform.transaction.Asset.IssuedAsset
+import com.wavesplatform.transaction.Transaction
+import com.wavesplatform.utils.ScorexLogging
+import org.scalatest.matchers.should.Matchers
+import play.api.libs.json.*
+import sttp.client3.*
+import sttp.client3.playJson.*
+import sttp.model.{StatusCode, Uri}
+import units.docker.WavesNodeContainer.MaxBlockDelay
+import units.test.IntegrationTestEventually
+
+class NodeHttpApi(apiUri: Uri, backend: SttpBackend[Identity, ?], apiKeyValue: String = DefaultApiKeyValue)
+ extends IntegrationTestEventually
+ with Matchers
+ with ScorexLogging {
+ def blockHeader(atHeight: Int)(implicit loggingOptions: LoggingOptions = LoggingOptions()): Option[BlockHeaderResponse] = {
+ if (loggingOptions.logRequest) log.debug(s"${loggingOptions.prefix} blockHeader($atHeight)")
+ basicRequest
+ .get(uri"$apiUri/blocks/headers/at/$atHeight")
+ .response(asJson[BlockHeaderResponse])
+ .tag(LoggingOptionsTag, loggingOptions)
+ .send(backend)
+ .body match {
+ case Left(HttpError(_, StatusCode.NotFound)) =>
+ if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} None")
+ none
+ case Left(HttpError(body, statusCode)) => fail(s"Server returned error $body with status ${statusCode.code}")
+ case Left(DeserializationException(body, error)) => fail(s"Failed to parse response $body: $error")
+ case Right(r) =>
+ if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} $r")
+ r.some
+ }
+ }
+
+ def waitForHeight(atLeast: Int)(implicit loggingOptions: LoggingOptions = LoggingOptions()): Height = {
+ if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} waitForHeight($atLeast)")
+ val subsequentLoggingOptions = loggingOptions.copy(logCall = false, logResult = false, logRequest = false)
+ val currHeight = height()(subsequentLoggingOptions)
+ if (currHeight >= atLeast) currHeight
+ else {
+ Thread.sleep(patienceConfig.interval.toMillis)
+ val waitBlocks = (atLeast - currHeight).min(1)
+ eventually(timeout(MaxBlockDelay * waitBlocks)) {
+ val h = height()(subsequentLoggingOptions)
+ h should be >= atLeast
+ if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} $h")
+ h
+ }
+ }
+ }
+
+ def height()(implicit loggingOptions: LoggingOptions = LoggingOptions()): Height = {
+ if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} height")
+ basicRequest
+ .get(uri"$apiUri/blocks/height")
+ .response(asJson[HeightResponse])
+ .tag(LoggingOptionsTag, loggingOptions)
+ .send(backend)
+ .body match {
+ case Left(e) => fail(e)
+ case Right(r) =>
+ if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} ${r.height}")
+ r.height
+ }
+ }
+
+ def broadcastAndWait(
+ txn: Transaction
+ )(implicit loggingOptions: LoggingOptions = LoggingOptions(logResponseBody = false)): TransactionInfoResponse = {
+ if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} broadcastAndWait")
+ broadcast(txn)(loggingOptions.copy(logRequest = false)).left.foreach { e =>
+ fail(s"Can't broadcast ${txn.id()}: code=${e.error}, message=${e.message}")
+ }
+ waitForSucceeded(txn.id())
+ }
+
+ def broadcast[T <: Transaction](txn: T)(implicit loggingOptions: LoggingOptions = LoggingOptions()): Either[ErrorResponse, T] = {
+ if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} broadcast($txn)")
+ basicRequest
+ .post(uri"$apiUri/transactions/broadcast")
+ .body(txn: Transaction)
+ .response(asJsonEither[ErrorResponse, BroadcastResponse])
+ .tag(LoggingOptionsTag, loggingOptions)
+ .send(backend)
+ .body match {
+ case Left(HttpError(e, _)) => e.asLeft
+ case Left(e) => fail(e)
+ case _ => txn.asRight
+ }
+ }
+
+ def waitForSucceeded(txnId: ByteStr)(implicit loggingOptions: LoggingOptions = LoggingOptions()): TransactionInfoResponse = {
+ if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} waitFor($txnId)")
+ var attempt = 0
+ eventually {
+ attempt += 1
+ val subsequentLoggingOptions = loggingOptions.copy(logCall = false, logResult = false, logRequest = attempt == 1)
+ transactionInfo(txnId)(subsequentLoggingOptions) match {
+ case Some(r) if r.applicationStatus == ApplicationStatus.Succeeded =>
+ if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} $r")
+ r
+
+ case r => fail(s"Expected ${ApplicationStatus.Succeeded} status, got: ${r.map(_.applicationStatus)}")
+ }
+ }
+ }
+
+ def transactionInfo(txnId: ByteStr)(implicit loggingOptions: LoggingOptions = LoggingOptions()): Option[TransactionInfoResponse] = {
+ if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} transactionInfo($txnId)")
+ basicRequest
+ .get(uri"$apiUri/transactions/info/$txnId")
+ .response(asJson[TransactionInfoResponse])
+ .tag(LoggingOptionsTag, loggingOptions)
+ .send(backend)
+ .body match {
+ case Left(HttpError(_, StatusCode.NotFound)) =>
+ if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} None")
+ none
+ case Left(HttpError(body, statusCode)) => fail(s"Server returned error $body with status ${statusCode.code}")
+ case Left(DeserializationException(body, error)) => fail(s"Failed to parse response $body: $error")
+ case Right(r) =>
+ if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} $r")
+ r.some
+ }
+ }
+
+ def dataByKey(address: Address, key: String)(implicit loggingOptions: LoggingOptions = LoggingOptions(logRequest = false)): Option[DataEntry[?]] = {
+ if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} dataByKey($address, $key)")
+ basicRequest
+ .get(uri"$apiUri/addresses/data/$address/$key")
+ .response(asJson[DataEntry[?]])
+ .tag(LoggingOptionsTag, loggingOptions)
+ .send(backend)
+ .body match {
+ case Left(HttpError(_, StatusCode.NotFound)) =>
+ if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} None")
+ none
+ case Left(HttpError(body, statusCode)) => fail(s"Server returned error $body with status ${statusCode.code}")
+ case Left(DeserializationException(body, error)) => fail(s"Failed to parse response $body: $error")
+ case Right(r) =>
+ if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} $r")
+ r match {
+ case _: EmptyDataEntry => none
+ case _ => r.some
+ }
+ }
+ }
+
+ def balance(address: Address, asset: IssuedAsset)(implicit loggingOptions: LoggingOptions = LoggingOptions()): Long = {
+ if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} balance($address, $asset)")
+ basicRequest
+ .get(uri"$apiUri/assets/balance/$address/$asset")
+ .response(asJson[AssetBalanceResponse])
+ .tag(LoggingOptionsTag, loggingOptions)
+ .send(backend)
+ .body match {
+ case Left(HttpError(_, StatusCode.NotFound)) =>
+ if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} 0")
+ 0L
+ case Left(HttpError(body, statusCode)) => fail(s"Server returned error $body with status ${statusCode.code}")
+ case Left(DeserializationException(body, error)) => fail(s"Failed to parse response $body: $error")
+ case Right(r) =>
+ if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} ${r.balance}")
+ r.balance
+ }
+ }
+
+ def assetQuantity(asset: IssuedAsset)(implicit loggingOptions: LoggingOptions = LoggingOptions()): Long = {
+ if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} assetQuantity($asset)")
+ basicRequest
+ .get(uri"$apiUri/assets/details/$asset?full=false")
+ .response(asJson[AssetDetailsResponse])
+ .tag(LoggingOptionsTag, loggingOptions)
+ .send(backend)
+ .body match {
+ case Left(HttpError(body, statusCode)) => fail(s"Server returned error $body with status ${statusCode.code}")
+ case Left(DeserializationException(body, error)) => fail(s"Failed to parse response $body: $error")
+ case Right(r) =>
+ if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} ${r.quantity}")
+ r.quantity
+ }
+ }
+
+ def evaluateExpr(address: Address, expr: String)(implicit loggingOptions: LoggingOptions = LoggingOptions(logRequest = false)): JsObject = {
+ if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} evaluateExpr($address, '$expr')")
+ basicRequest
+ .post(uri"$apiUri/utils/script/evaluate/$address")
+ .body(Json.obj("expr" -> expr))
+ .response(asJson[JsObject])
+ .tag(LoggingOptionsTag, loggingOptions)
+ .send(backend)
+ .body match {
+ case Left(e) => fail(e)
+ case Right(r) =>
+ if (loggingOptions.logResult) log.debug(s"${loggingOptions.prefix} ${(r \ "result").getOrElse(r)}")
+ r
+ }
+ }
+
+ def createWalletAddress()(implicit loggingOptions: LoggingOptions = LoggingOptions()): Unit = {
+ if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} createWalletAddress")
+ basicRequest
+ .post(uri"$apiUri/addresses")
+ .header(`X-Api-Key`.name, apiKeyValue)
+ .response(asString)
+ .tag(LoggingOptionsTag, loggingOptions)
+ .send(backend)
+ }
+
+ def rollback(to: Height)(implicit loggingOptions: LoggingOptions = LoggingOptions()): Unit = {
+ if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} rollback($to)")
+ basicRequest
+ .post(uri"$apiUri/debug/rollback")
+ .header(`X-Api-Key`.name, apiKeyValue)
+ .body(
+ Json.obj(
+ "rollbackTo" -> to,
+ "returnTransactionsToUtx" -> false
+ )
+ )
+ .response(asString)
+ .tag(LoggingOptionsTag, loggingOptions)
+ .send(backend)
+ }
+
+ def print(message: String): Unit =
+ basicRequest
+ .post(uri"$apiUri/debug/print")
+ .header(`X-Api-Key`.name, apiKeyValue)
+ .body(Json.obj("message" -> message))
+ .response(ignore)
+ .send(backend)
+}
+
+object NodeHttpApi {
+ val DefaultApiKeyValue = "testapi"
+
+ case class BlockHeaderResponse(VRF: String)
+ object BlockHeaderResponse {
+ implicit val blockHeaderResponseFormat: OFormat[BlockHeaderResponse] = Json.format[BlockHeaderResponse]
+ }
+
+ case class HeightResponse(height: Height)
+ object HeightResponse {
+ implicit val heightResponseFormat: OFormat[HeightResponse] = Json.format[HeightResponse]
+ }
+
+ case class BroadcastResponse(id: String)
+ object BroadcastResponse {
+ implicit val broadcastResponseFormat: OFormat[BroadcastResponse] = Json.format[BroadcastResponse]
+ }
+
+ case class TransactionInfoResponse(height: Height, applicationStatus: String)
+ object TransactionInfoResponse {
+ implicit val transactionInfoResponseFormat: OFormat[TransactionInfoResponse] = Json.format[TransactionInfoResponse]
+ }
+
+ case class AssetBalanceResponse(balance: Long)
+ object AssetBalanceResponse {
+ implicit val assetBalanceResponseFormat: OFormat[AssetBalanceResponse] = Json.format[AssetBalanceResponse]
+ }
+
+ case class AssetDetailsResponse(quantity: Long)
+ object AssetDetailsResponse {
+ implicit val assetDetailsResponseFormat: OFormat[AssetDetailsResponse] = Json.format[AssetDetailsResponse]
+ }
+
+ case class ConnectedPeersResponse(peers: List[JsObject])
+ object ConnectedPeersResponse {
+ implicit val connectedPeersResponseFormat: OFormat[ConnectedPeersResponse] = Json.format[ConnectedPeersResponse]
+ }
+
+ case class ErrorResponse(error: Int, message: String)
+ object ErrorResponse {
+ implicit val errorResponseFormat: OFormat[ErrorResponse] = Json.format[ErrorResponse]
+ }
+}
diff --git a/consensus-client-it/src/test/scala/org/scalatest/enablers/ConstantRetrying.scala b/consensus-client-it/src/test/scala/org/scalatest/enablers/ConstantRetrying.scala
new file mode 100644
index 00000000..a8235129
--- /dev/null
+++ b/consensus-client-it/src/test/scala/org/scalatest/enablers/ConstantRetrying.scala
@@ -0,0 +1,56 @@
+package org.scalatest.enablers
+
+import org.scalactic.source
+import org.scalatest.Resources
+import org.scalatest.Suite.anExceptionThatShouldCauseAnAbort
+import org.scalatest.exceptions.{StackDepthException, TestFailedDueToTimeoutException, TestPendingException}
+import org.scalatest.time.{Nanosecond, Span}
+
+import scala.annotation.tailrec
+
+// Retrying with a constant intervals. Copy-paste from Retrying.retryingNatureOfT without excess logic
+object ConstantRetrying {
+ def create[T]: Retrying[T] = new Retrying[T] {
+ override def retry(timeout: Span, interval: Span, pos: source.Position)(fun: => T): T = {
+ val startNanos = System.nanoTime
+ def makeAValiantAttempt(): Either[Throwable, T] = {
+ try {
+ Right(fun)
+ } catch {
+ case tpe: TestPendingException => throw tpe
+ case e: Throwable if !anExceptionThatShouldCauseAnAbort(e) => Left(e)
+ }
+ }
+
+ @tailrec
+ def tryTryAgain(attempt: Int): T = {
+ makeAValiantAttempt() match {
+ case Right(result) => result
+ case Left(e) =>
+ val duration = System.nanoTime - startNanos
+ if (duration < timeout.totalNanos) {
+ Thread.sleep(interval.millisPart, interval.nanosPart)
+ } else {
+ val durationSpan = Span(1, Nanosecond).scaledBy(duration.toDouble) // Use scaledBy to get pretty units
+ throw new TestFailedDueToTimeoutException(
+ (_: StackDepthException) =>
+ Some(
+ if (e.getMessage == null)
+ Resources.didNotEventuallySucceed(attempt.toString, durationSpan.prettyString)
+ else
+ Resources.didNotEventuallySucceedBecause(attempt.toString, durationSpan.prettyString, e.getMessage)
+ ),
+ Some(e),
+ Left(pos),
+ None,
+ timeout
+ )
+ }
+
+ tryTryAgain(attempt + 1)
+ }
+ }
+ tryTryAgain(1)
+ }
+ }
+}
diff --git a/consensus-client-it/src/test/scala/units/Accounts.scala b/consensus-client-it/src/test/scala/units/Accounts.scala
new file mode 100644
index 00000000..227e60d0
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/Accounts.scala
@@ -0,0 +1,33 @@
+package units
+
+import com.google.common.primitives.{Bytes, Ints}
+import com.wavesplatform.account.{KeyPair, SeedKeyPair}
+import com.wavesplatform.crypto
+import org.web3j.crypto.Credentials
+import units.eth.EthAddress
+
+import java.nio.charset.StandardCharsets
+
+trait Accounts {
+ val chainContractAccount: KeyPair = mkKeyPair("devnet cc", 0)
+
+ val miner11Account = mkKeyPair("devnet-1", 0)
+ val miner11RewardAddress = EthAddress.unsafeFrom("0x7dbcf9c6c3583b76669100f9be3caf6d722bc9f9")
+
+ val miner12Account = mkKeyPair("devnet-1", 1)
+ val miner12RewardAddress = EthAddress.unsafeFrom("0x7dbcf9c6c3583b76669100f9be3caf6d722bc9f0")
+
+ val miner21Account = mkKeyPair("devnet-2", 0)
+ val miner21RewardAddress = EthAddress.unsafeFrom("0xcf0b9e13fdd593f4ca26d36afcaa44dd3fdccbed")
+
+ val clRichAccount1 = mkKeyPair("devnet rich", 0)
+ val clRichAccount2 = mkKeyPair("devnet rich", 1)
+
+ val elBridgeAddress = EthAddress.unsafeFrom("0x0000000000000000000000000000000000006a7e")
+
+ val elRichAccount1 = Credentials.create("8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63")
+ val elRichAccount2 = Credentials.create("ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f")
+
+ protected def mkKeyPair(seed: String, nonce: Int): SeedKeyPair =
+ SeedKeyPair(crypto.secureHash(Bytes.concat(Ints.toByteArray(nonce), seed.getBytes(StandardCharsets.UTF_8))))
+}
diff --git a/consensus-client-it/src/test/scala/units/AlternativeChainTestSuite.scala b/consensus-client-it/src/test/scala/units/AlternativeChainTestSuite.scala
new file mode 100644
index 00000000..835f0124
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/AlternativeChainTestSuite.scala
@@ -0,0 +1,60 @@
+package units
+
+import com.wavesplatform.account.KeyPair
+import com.wavesplatform.api.LoggingBackend.LoggingOptions
+import com.wavesplatform.api.http.ApiError.ScriptExecutionError
+import com.wavesplatform.common.state.ByteStr
+import units.client.contract.HasConsensusLayerDappTxHelpers.EmptyE2CTransfersRootHashHex
+
+import scala.annotation.tailrec
+
+class AlternativeChainTestSuite extends BaseDockerTestSuite {
+ "L2-383 Start an alternative chain after not getting an EL-block" in {
+ step(s"Wait miner 1 (${miner11Account.toAddress}) forge at least one block")
+ chainContract.waitForHeight(1L)
+
+ step(s"EL miner 2 (${miner21Account.toAddress}) join")
+ val heightBeforeJoin = waves1.api.height()
+ waves1.api.broadcastAndWait(
+ ChainContract.join(
+ minerAccount = miner21Account,
+ elRewardAddress = miner21RewardAddress
+ )
+ )
+
+ step(s"Wait miner 2 (${miner21Account.toAddress}) epoch and issue a block confirmation")
+ waves1.api.waitForHeight(heightBeforeJoin + 1)
+ broadcastElBlockConfirmation(miner21Account)
+
+ step(s"Wait miner 1 (${miner11Account.toAddress}) epoch")
+ chainContract.waitForMinerEpoch(miner11Account)
+
+ step("Checking an alternative chain started")
+ chainContract.waitForChainId(1L)
+ }
+
+ @tailrec private def broadcastElBlockConfirmation(minerAccount: KeyPair, maxAttempts: Int = 5)(implicit
+ loggingOptions: LoggingOptions = LoggingOptions(logRequest = false)
+ ): Unit = {
+ if (maxAttempts == 0) fail("Can't broadcast an EL-block confirmation: all attempts are out")
+
+ chainContract.waitForMinerEpoch(minerAccount)
+ val lastContractBlock = chainContract.getLastBlockMeta(0).value
+ val lastWavesBlock = waves1.api.blockHeader(waves1.api.height()).value
+ val txn = ChainContract.extendMainChain(
+ minerAccount = minerAccount,
+ blockHash = BlockHash("0x0000000000000000000000000000000000000000000000000000000000000001"),
+ parentBlockHash = lastContractBlock.hash,
+ e2cTransfersRootHashHex = EmptyE2CTransfersRootHashHex,
+ lastC2ETransferIndex = -1,
+ vrf = ByteStr.decodeBase58(lastWavesBlock.VRF).get
+ )
+ waves1.api.broadcast(txn) match {
+ case Left(e) if e.error == ScriptExecutionError.Id =>
+ log.debug(s"Failed to send an EL-block confirmation: $e")
+ broadcastElBlockConfirmation(minerAccount, maxAttempts - 1)
+ case Left(e) => fail(s"Can't broadcast an EL-block confirmation: $e")
+ case _ => waves1.api.waitForSucceeded(txn.id())
+ }
+ }
+}
diff --git a/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala b/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala
new file mode 100644
index 00000000..2cce8fb5
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/BaseDockerTestSuite.scala
@@ -0,0 +1,142 @@
+package units
+
+import com.wavesplatform.account.AddressScheme
+import com.wavesplatform.api.LoggingBackend
+import com.wavesplatform.common.state.ByteStr
+import com.wavesplatform.common.utils.EitherExt2
+import com.wavesplatform.utils.ScorexLogging
+import monix.execution.atomic.AtomicBoolean
+import org.scalatest.freespec.AnyFreeSpec
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.{BeforeAndAfterAll, EitherValues, OptionValues}
+import sttp.client3.{HttpClientSyncBackend, Identity, SttpBackend}
+import units.client.HttpChainContractClient
+import units.client.contract.HasConsensusLayerDappTxHelpers
+import units.client.engine.model.BlockNumber
+import units.docker.*
+import units.docker.WavesNodeContainer.generateWavesGenesisConfig
+import units.el.ElBridgeClient
+import units.eth.Gwei
+import units.test.{CustomMatchers, IntegrationTestEventually, TestEnvironment}
+
+trait BaseDockerTestSuite
+ extends AnyFreeSpec
+ with ScorexLogging
+ with BeforeAndAfterAll
+ with Matchers
+ with CustomMatchers
+ with EitherValues
+ with OptionValues
+ with ReportingTestName
+ with IntegrationTestEventually
+ with Accounts
+ with HasConsensusLayerDappTxHelpers {
+ override val currentHitSource: ByteStr = ByteStr.empty
+ protected val rewardAmount: Gwei = Gwei.ofRawGwei(2_000_000_000L)
+
+ protected lazy val network = Networks.network
+
+ protected lazy val wavesGenesisConfigPath = generateWavesGenesisConfig()
+
+ private implicit val httpClientBackend: SttpBackend[Identity, Any] = new LoggingBackend(HttpClientSyncBackend())
+
+ protected lazy val ec1: EcContainer = {
+ val constructor = TestEnvironment.ExecutionClient match {
+ case "besu" => new BesuContainer(_, _, _)
+ case "geth" => new GethContainer(_, _, _)
+ case x => throw new RuntimeException(s"Unknown execution client: $x. Only 'geth' or 'besu' supported")
+ }
+
+ constructor(network, 1, Networks.ipForNode(2) /* ipForNode(1) is assigned to Ryuk */ )
+ }
+
+ protected lazy val waves1: WavesNodeContainer = new WavesNodeContainer(
+ network = network,
+ number = 1,
+ ip = Networks.ipForNode(3),
+ baseSeed = "devnet-1",
+ chainContractAddress = chainContractAddress,
+ ecEngineApiUrl = ec1.engineApiDockerUrl,
+ genesisConfigPath = wavesGenesisConfigPath
+ )
+
+ protected lazy val chainContract = new HttpChainContractClient(waves1.api, chainContractAddress)
+ protected lazy val elBridge = new ElBridgeClient(ec1.web3j, elBridgeAddress)
+
+ protected def startNodes(): Unit = {
+ ec1.start()
+ ec1.logPorts()
+
+ waves1.start()
+ waves1.waitReady()
+ waves1.logPorts()
+ }
+
+ protected def stopNodes(): Unit = {
+ waves1.stop()
+ ec1.stop()
+ }
+
+ protected def setupChain(): Unit = {
+ log.info("Set script")
+ waves1.api.broadcastAndWait(ChainContract.setScript())
+
+ log.info("Setup chain contract")
+ val genesisBlock = ec1.engineApi.getBlockByNumber(BlockNumber.Number(0)).explicitGet().getOrElse(fail("No EL genesis block"))
+ waves1.api.broadcastAndWait(
+ ChainContract.setup(
+ genesisBlock = genesisBlock,
+ elMinerReward = rewardAmount.amount.longValue(),
+ daoAddress = None,
+ daoReward = 0,
+ invoker = chainContractAccount
+ )
+ )
+ log.info(s"Token id: ${chainContract.token}")
+
+ log.info("EL miner #1 join")
+ val joinMiner1Result = waves1.api.broadcastAndWait(
+ ChainContract.join(
+ minerAccount = miner11Account,
+ elRewardAddress = miner11RewardAddress
+ )
+ )
+
+ val epoch1Number = joinMiner1Result.height + 1
+ log.info(s"Wait for #$epoch1Number epoch")
+ waves1.api.waitForHeight(epoch1Number)
+ }
+
+ override protected def step(text: String): Unit = {
+ super.step(text)
+ waves1.api.print(text)
+ // ec1.web3j // Hove no idea how to do this
+ }
+
+ override def beforeAll(): Unit = {
+ BaseDockerTestSuite.init()
+ super.beforeAll()
+ log.debug(s"Docker network name: ${network.getName}, id: ${network.getId}") // Force create network
+
+ startNodes()
+ setupChain()
+ }
+
+ override protected def afterAll(): Unit = {
+ httpClientBackend.close()
+
+ stopNodes()
+ network.close()
+ super.afterAll()
+ }
+}
+
+object BaseDockerTestSuite {
+ private val initialized = AtomicBoolean(false)
+
+ def init(): Unit =
+ if (initialized.compareAndSet(expect = false, update = true))
+ AddressScheme.current = new AddressScheme {
+ override val chainId: Byte = 'D'.toByte
+ }
+}
diff --git a/consensus-client-it/src/test/scala/units/BridgeC2ETestSuite.scala b/consensus-client-it/src/test/scala/units/BridgeC2ETestSuite.scala
new file mode 100644
index 00000000..018bb955
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/BridgeC2ETestSuite.scala
@@ -0,0 +1,79 @@
+package units
+
+import com.wavesplatform.transaction.TxHelpers
+import units.client.engine.model.BlockNumber
+import units.eth.EthAddress
+
+class BridgeC2ETestSuite extends BaseDockerTestSuite {
+ private val clSender = clRichAccount1
+ private val elReceiver = elRichAccount1
+ private val elReceiverAddress = EthAddress.unsafeFrom(elReceiver.getAddress)
+ private val userAmount = 1
+ private val wavesAmount = UnitsConvert.toWavesAmount(userAmount)
+ private val gweiAmount = UnitsConvert.toGwei(userAmount)
+
+ "L2-380 Checking balances in CL->EL transfers" in {
+ def clAssetQuantity: Long = waves1.api.assetQuantity(chainContract.token)
+ def chainContractBalance: Long = waves1.api.balance(chainContractAddress, chainContract.token)
+
+ val clAssetQuantityBefore = clAssetQuantity
+ val chainContractBalanceBefore = chainContractBalance
+
+ val elCurrHeight = ec1.web3j.ethBlockNumber().send().getBlockNumber.intValueExact()
+
+ waves1.api.broadcastAndWait(
+ ChainContract.transfer(
+ sender = clSender,
+ destElAddress = elReceiverAddress,
+ asset = chainContract.token,
+ amount = wavesAmount
+ )
+ )
+
+ val chainContractBalanceAfter = chainContractBalance
+ withClue("1. Chain contract balance unchanged: ") {
+ chainContractBalanceAfter shouldBe chainContractBalanceBefore
+ }
+
+ val clAssetQuantityAfter = clAssetQuantity
+ withClue("1. Tokens burned: ") {
+ clAssetQuantityAfter shouldBe (clAssetQuantityBefore - wavesAmount)
+ }
+
+ val withdrawal = Iterator
+ .from(elCurrHeight + 1)
+ .map { h =>
+ eventually {
+ ec1.engineApi.getBlockByNumber(BlockNumber.Number(h)).toOption.flatten.value
+ }
+ }
+ .flatMap(_.withdrawals)
+ .find(_.address == elReceiverAddress)
+ .head
+
+ withClue("2. Expected amount: ") {
+ withdrawal.amount shouldBe gweiAmount
+ }
+ }
+
+ override def beforeAll(): Unit = {
+ super.beforeAll()
+
+ step("Prepare: issue tokens on chain contract and transfer to a user")
+ waves1.api.broadcastAndWait(
+ TxHelpers.reissue(
+ asset = chainContract.token,
+ sender = chainContractAccount,
+ amount = wavesAmount
+ )
+ )
+ waves1.api.broadcastAndWait(
+ TxHelpers.transfer(
+ from = chainContractAccount,
+ to = clSender.toAddress,
+ amount = wavesAmount,
+ asset = chainContract.token
+ )
+ )
+ }
+}
diff --git a/consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala b/consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala
new file mode 100644
index 00000000..c0221452
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/BridgeE2CTestSuite.scala
@@ -0,0 +1,127 @@
+package units
+
+import com.wavesplatform.common.utils.EitherExt2
+import com.wavesplatform.utils.EthEncoding
+import org.web3j.protocol.core.DefaultBlockParameterName
+import org.web3j.protocol.core.methods.response.TransactionReceipt
+import org.web3j.protocol.exceptions.TransactionException
+import org.web3j.utils.Convert
+import units.el.ElBridgeClient
+
+class BridgeE2CTestSuite extends BaseDockerTestSuite {
+ private val elSender = elRichAccount1
+ private val clRecipient = clRichAccount1
+ private val userAmount = 1
+ private val wavesAmount = UnitsConvert.toWavesAmount(userAmount)
+
+ private def sendNative(amount: BigInt = UnitsConvert.toWei(userAmount)): TransactionReceipt =
+ elBridge.sendNative(elSender, clRecipient.toAddress, amount)
+
+ private val tenGwei = BigInt(Convert.toWei("10", Convert.Unit.GWEI).toBigIntegerExact)
+
+ "Negative" - {
+ def sendNativeInvalid(amount: BigInt): TransactionException =
+ try {
+ sendNative(amount)
+ fail(s"Expected sendNative($amount) to fail")
+ } catch {
+ case e: TransactionException => e
+ }
+
+ "L2-264 Amount should % 10 Gwei" in {
+ val e = sendNativeInvalid(tenGwei + 1)
+ val encodedRevertReason = e.getTransactionReceipt.get().getRevertReason
+ val revertReason = ElBridgeClient.decodeRevertReason(encodedRevertReason)
+ revertReason shouldBe "Sent value 10000000001 must be a multiple of 10000000000"
+ }
+
+ "L2-265 Amount should be between 10 and MAX_AMOUNT_IN_WEI Gwei" in {
+ withClue("1. Less than 10 Gwei: ") {
+ val e = sendNativeInvalid(1)
+ val encodedRevertReason = e.getTransactionReceipt.get().getRevertReason
+ val revertReason = ElBridgeClient.decodeRevertReason(encodedRevertReason)
+ revertReason shouldBe "Sent value 1 must be greater or equal to 10000000000"
+ }
+
+ withClue("2. More than MAX_AMOUNT_IN_WEI: ") {
+ val maxAmountInWei = BigInt(Long.MaxValue) * tenGwei
+ val biggerAmount = (maxAmountInWei / tenGwei + 1) * tenGwei
+ val e = sendNativeInvalid(biggerAmount)
+ val encodedRevertReason = e.getTransactionReceipt.get().getRevertReason
+ val revertReason = ElBridgeClient.decodeRevertReason(encodedRevertReason)
+ revertReason shouldBe s"Sent value $biggerAmount must be less or equal to $maxAmountInWei"
+ }
+ }
+ }
+
+ "L2-325 Sent tokens burned" in {
+ def burnedTokens = ec1.web3j.ethGetBalance(ElBridgeClient.BurnAddress.hex, DefaultBlockParameterName.LATEST).send().getBalance
+ val burnedTokensBefore = BigInt(burnedTokens)
+
+ val transferAmount = tenGwei
+ sendNative(transferAmount)
+ val burnedTokensAfter = BigInt(burnedTokens)
+
+ burnedTokensAfter shouldBe (transferAmount + burnedTokensBefore)
+ }
+
+ "L2-379 Checking balances in EL->CL transfers" in {
+ step("Broadcast Bridge.sendNative transaction")
+ def bridgeBalance = ec1.web3j.ethGetBalance(elBridgeAddress.hex, DefaultBlockParameterName.LATEST).send().getBalance
+ val bridgeBalanceBefore = bridgeBalance
+ val sendTxnReceipt = sendNative()
+
+ withClue("1. The balance of Bridge contract wasn't changed: ") {
+ val bridgeBalanceAfter = bridgeBalance
+ bridgeBalanceAfter shouldBe bridgeBalanceBefore
+ }
+
+ val blockHash = BlockHash(sendTxnReceipt.getBlockHash)
+ step(s"Block with transaction: $blockHash")
+
+ val logsInBlock = ec1.engineApi.getLogs(blockHash, elBridgeAddress, Bridge.ElSentNativeEventTopic).explicitGet()
+
+ val transferEvents = logsInBlock.map { x =>
+ Bridge.ElSentNativeEvent.decodeArgs(x.data).explicitGet()
+ }
+ step(s"Transfer events: ${transferEvents.mkString(", ")}")
+
+ val sendTxnLogIndex = logsInBlock.indexWhere(_.transactionHash == sendTxnReceipt.getTransactionHash)
+ val transferProofs = Bridge.mkTransferProofs(transferEvents, sendTxnLogIndex).reverse
+
+ step(s"Wait block $blockHash on contract")
+ val blockConfirmationHeight = eventually {
+ chainContract.getBlock(blockHash).value.height
+ }
+
+ step(s"Wait block $blockHash ($blockConfirmationHeight) finalization")
+ eventually {
+ val currFinalizedHeight = chainContract.getFinalizedBlock.height
+ step(s"Current finalized height: $currFinalizedHeight")
+ currFinalizedHeight should be >= blockConfirmationHeight
+ }
+
+ withClue("3. Tokens received: ") {
+ step(
+ s"Broadcast withdraw transaction: transferIndexInBlock=$sendTxnLogIndex, amount=$wavesAmount, " +
+ s"merkleProof={${transferProofs.map(EthEncoding.toHexString).mkString(",")}}"
+ )
+
+ def receiverBalance: Long = waves1.api.balance(clRecipient.toAddress, chainContract.token)
+ val receiverBalanceBefore = receiverBalance
+
+ waves1.api.broadcastAndWait(
+ ChainContract.withdraw(
+ sender = clRecipient,
+ blockHash = BlockHash(sendTxnReceipt.getBlockHash),
+ merkleProof = transferProofs,
+ transferIndexInBlock = sendTxnLogIndex,
+ amount = wavesAmount
+ )
+ )
+
+ val balanceAfter = receiverBalance
+ balanceAfter shouldBe (receiverBalanceBefore + wavesAmount)
+ }
+ }
+}
diff --git a/consensus-client-it/src/test/scala/units/ReportingTestName.scala b/consensus-client-it/src/test/scala/units/ReportingTestName.scala
new file mode 100644
index 00000000..bef857c2
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/ReportingTestName.scala
@@ -0,0 +1,18 @@
+package units
+
+import org.scalatest.{Args, Status, SuiteMixin}
+
+trait ReportingTestName extends SuiteMixin {
+ self: BaseDockerTestSuite =>
+
+ abstract override protected def runTest(testName: String, args: Args): Status = {
+ testStep(s"Test '$testName' started")
+ val r = super.runTest(testName, args)
+ testStep(s"Test '$testName' ${if (r.succeeds()) "SUCCEEDED" else "FAILED"}")
+ r
+ }
+
+ private def testStep(text: String): Unit = step(s"---------- $text ----------")
+
+ protected def step(text: String): Unit = log.debug(text)
+}
diff --git a/consensus-client-it/src/test/scala/units/RewardTestSuite.scala b/consensus-client-it/src/test/scala/units/RewardTestSuite.scala
new file mode 100644
index 00000000..6a857298
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/RewardTestSuite.scala
@@ -0,0 +1,44 @@
+package units
+
+import units.client.engine.model.BlockNumber
+
+class RewardTestSuite extends BaseDockerTestSuite {
+ "L2-234 The reward for a previous epoch is in the first block withdrawals" in {
+ val epoch1FirstEcBlock = eventually {
+ ec1.engineApi.getBlockByNumber(BlockNumber.Number(1)).value.value
+ }
+
+ withClue("No reward for genesis block: ") {
+ epoch1FirstEcBlock.withdrawals shouldBe empty
+ }
+
+ val epoch1FirstContractBlock = eventually {
+ chainContract.getBlock(epoch1FirstEcBlock.hash).value
+ }
+
+ val epoch1Number = epoch1FirstContractBlock.epoch
+ val epoch2Number = epoch1Number + 1
+
+ waves1.api.waitForHeight(epoch2Number)
+
+ step(s"Wait for epoch #$epoch2Number data on chain contract")
+ val epoch2FirstContractBlock = eventually {
+ chainContract.getEpochFirstBlock(epoch2Number).value
+ }
+
+ val epoch2FirstEcBlock = ec1.engineApi
+ .getBlockByHash(epoch2FirstContractBlock.hash)
+ .value
+ .value
+
+ epoch2FirstEcBlock.withdrawals should have length 1
+
+ withClue("Expected reward amount: ") {
+ epoch2FirstEcBlock.withdrawals(0).amount shouldBe rewardAmount
+ }
+
+ withClue("Expected reward receiver: ") {
+ epoch2FirstEcBlock.withdrawals(0).address shouldBe miner11RewardAddress
+ }
+ }
+}
diff --git a/consensus-client-it/src/test/scala/units/SyncingTestSuite.scala b/consensus-client-it/src/test/scala/units/SyncingTestSuite.scala
new file mode 100644
index 00000000..311130b3
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/SyncingTestSuite.scala
@@ -0,0 +1,86 @@
+package units
+
+import com.wavesplatform.utils.EthEncoding
+import org.web3j.crypto.{RawTransaction, TransactionEncoder}
+import org.web3j.protocol.core.DefaultBlockParameter
+import org.web3j.protocol.core.methods.response.{EthSendTransaction, TransactionReceipt}
+import org.web3j.tx.gas.DefaultGasProvider
+import org.web3j.utils.Convert
+import units.docker.EcContainer
+
+import java.math.BigInteger
+import scala.jdk.OptionConverters.RichOptional
+
+class SyncingTestSuite extends BaseDockerTestSuite {
+ private val elSender = elRichAccount1
+ private val amount = Convert.toWei("1", Convert.Unit.ETHER).toBigInteger
+
+ // TODO: Will be removed after fixing geth issues
+ "L2-381 EL transactions appear after rollback" ignore {
+ step("Send transaction 1")
+ val txn1Result = sendTxn(0)
+ waitForTxn(txn1Result)
+
+ val height1 = waves1.api.height()
+
+ step("Wait for next epoch")
+ waves1.api.waitForHeight(height1 + 1)
+
+ step("Send transactions 2 and 3")
+ val txn2Result = sendTxn(1)
+ val txn3Result = sendTxn(2)
+
+ val txn2Receipt = waitForTxn(txn2Result)
+ val txn3Receipt = waitForTxn(txn3Result)
+
+ val blocksWithTxns = List(txn2Receipt, txn3Receipt).map(x => x.getBlockNumber -> x.getBlockHash).toMap
+ step(s"Waiting blocks ${blocksWithTxns.mkString(", ")} on contract")
+ blocksWithTxns.foreach { case (_, blockHash) =>
+ eventually {
+ chainContract.getBlock(BlockHash(blockHash)).value
+ }
+ }
+
+ step("Rollback CL")
+ waves1.api.rollback(height1)
+
+ step("Wait for EL mining")
+ waves1.api.waitForHeight(height1 + 2)
+
+ step(s"Waiting blocks ${blocksWithTxns.mkString(", ")} disappear")
+ blocksWithTxns.foreach { case (_, blockHash) =>
+ eventually {
+ chainContract.getBlock(BlockHash(blockHash)) shouldBe empty
+ }
+ }
+
+ blocksWithTxns.foreach { case (height, blockHash) =>
+ eventually {
+ val block = ec1.web3j.ethGetBlockByNumber(DefaultBlockParameter.valueOf(height), false).send().getBlock
+ block.getHash shouldNot be(blockHash)
+ }
+ }
+
+ step("Waiting transactions 2 and 3 on EL")
+ waitForTxn(txn2Result)
+ waitForTxn(txn3Result)
+ }
+
+ private def sendTxn(nonce: Long): EthSendTransaction = {
+ val rawTransaction = RawTransaction.createEtherTransaction(
+ EcContainer.ChainId,
+ BigInteger.valueOf(nonce),
+ DefaultGasProvider.GAS_LIMIT,
+ "0x0000000000000000000000000000000000000000",
+ amount,
+ BigInteger.ZERO,
+ DefaultGasProvider.GAS_PRICE
+ )
+ val signedTransaction = EthEncoding.toHexString(TransactionEncoder.signMessage(rawTransaction, elSender))
+ ec1.web3j.ethSendRawTransaction(signedTransaction).send()
+ }
+
+ private def waitForTxn(txnResult: EthSendTransaction): TransactionReceipt = eventually {
+ ec1.web3j.ethGetTransactionReceipt(txnResult.getTransactionHash).send().getTransactionReceipt.toScala.value
+ }
+}
diff --git a/consensus-client-it/src/test/scala/units/UnitsConvert.scala b/consensus-client-it/src/test/scala/units/UnitsConvert.scala
new file mode 100644
index 00000000..367a41ce
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/UnitsConvert.scala
@@ -0,0 +1,17 @@
+package units
+
+import com.wavesplatform.settings.Constants
+import org.web3j.utils.Convert
+import units.eth.Gwei
+
+import java.math.BigInteger
+
+object UnitsConvert {
+ def toWavesAmount(userAmount: BigDecimal): Long = (userAmount * Constants.UnitsInWave).toLongExact
+ def toWei(userAmount: BigDecimal): BigInteger = Convert.toWei(userAmount.bigDecimal, Convert.Unit.ETHER).toBigIntegerExact
+ def toGwei(userAmount: BigDecimal): Gwei = {
+ val rawWei = Convert.toWei(userAmount.bigDecimal, Convert.Unit.ETHER)
+ val rawGwei = Convert.fromWei(rawWei, Convert.Unit.GWEI).longValue()
+ Gwei.ofRawGwei(rawGwei)
+ }
+}
diff --git a/consensus-client-it/src/test/scala/units/client/HttpChainContractClient.scala b/consensus-client-it/src/test/scala/units/client/HttpChainContractClient.scala
new file mode 100644
index 00000000..8bea9d06
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/client/HttpChainContractClient.scala
@@ -0,0 +1,90 @@
+package units.client
+
+import cats.syntax.option.*
+import com.wavesplatform.account.{Address, KeyPair}
+import com.wavesplatform.api.LoggingBackend.LoggingOptions
+import com.wavesplatform.api.NodeHttpApi
+import com.wavesplatform.common.state.ByteStr
+import com.wavesplatform.state.DataEntry
+import com.wavesplatform.transaction.Asset.IssuedAsset
+import com.wavesplatform.utils.ScorexLogging
+import org.scalatest.OptionValues
+import org.scalatest.matchers.should.Matchers
+import units.BlockHash
+import units.client.contract.ChainContractClient.DefaultMainChainId
+import units.client.contract.{ChainContractClient, ContractBlock}
+import units.docker.WavesNodeContainer
+import units.test.IntegrationTestEventually
+
+import scala.annotation.tailrec
+
+class HttpChainContractClient(api: NodeHttpApi, override val contract: Address)
+ extends ChainContractClient
+ with IntegrationTestEventually
+ with Matchers
+ with OptionValues
+ with ScorexLogging {
+ override def extractData(key: String): Option[DataEntry[?]] = api.dataByKey(contract, key)(LoggingOptions(logRequest = false))
+
+ private val fiveBlocks = WavesNodeContainer.MaxBlockDelay * 5
+ lazy val token: IssuedAsset = IssuedAsset(ByteStr.decodeBase58(getStringData("tokenId").getOrElse(fail("Call setup first"))).get)
+
+ def getEpochFirstBlock(epochNumber: Int): Option[ContractBlock] =
+ getEpochMeta(epochNumber).flatMap { epochData =>
+ getEpochFirstBlock(epochData.lastBlockHash)
+ }
+
+ def getEpochFirstBlock(blockHashInEpoch: BlockHash): Option[ContractBlock] = {
+ @tailrec
+ def loop(lastBlock: ContractBlock): Option[ContractBlock] =
+ getBlock(lastBlock.parentHash) match {
+ case Some(blockData) =>
+ if (blockData.height == 0) blockData.some
+ else if (blockData.epoch == lastBlock.epoch) loop(blockData)
+ else lastBlock.some
+
+ case None => none
+ }
+
+ getBlock(blockHashInEpoch).flatMap { blockData =>
+ if (blockData.height == 0) blockData.some
+ else loop(blockData)
+ }
+ }
+
+ def waitForMinerEpoch(minerAccount: KeyPair)(implicit loggingOptions: LoggingOptions = LoggingOptions(logRequest = false)): Unit = {
+ val expectedGenerator = minerAccount.toAddress
+ if (loggingOptions.logCall) log.debug(s"${loggingOptions.prefix} waitMinerEpoch($expectedGenerator)")
+
+ val subsequentLoggingOptions = loggingOptions.copy(logCall = false)
+ eventually(timeout(fiveBlocks)) {
+ val actualGenerator = computedGenerator()(subsequentLoggingOptions)
+ actualGenerator shouldBe expectedGenerator
+ }
+ }
+
+ def waitForHeight(atLeast: Long, chainId: Long = DefaultMainChainId): Unit = {
+ log.debug(s"waitForHeight($atLeast)")
+ eventually {
+ val lastBlock = getLastBlockMeta(chainId).value
+ lastBlock.height should be >= atLeast
+ }
+ }
+
+ def waitForChainId(chainId: Long): Unit = {
+ log.debug(s"waitForChainId($chainId)")
+ eventually {
+ getFirstBlockHash(chainId) shouldBe defined
+ }
+ }
+
+ def computedGenerator()(implicit loggingOptions: LoggingOptions): Address = {
+ if (loggingOptions.logCall) log.debug("computedGenerator")
+ val rawResult = api.evaluateExpr(contract, "computedGenerator").result
+ val rawAddress = (rawResult \ "result" \ "value").as[String]
+ Address.fromString(rawAddress) match {
+ case Left(e) => fail(s"Can't parse computedGenerator address: $rawAddress. Reason: $e")
+ case Right(r) => r
+ }
+ }
+}
diff --git a/consensus-client-it/src/test/scala/units/docker/BaseContainer.scala b/consensus-client-it/src/test/scala/units/docker/BaseContainer.scala
new file mode 100644
index 00000000..484f80e1
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/docker/BaseContainer.scala
@@ -0,0 +1,25 @@
+package units.docker
+
+import com.wavesplatform.utils.LoggerFacade
+import org.slf4j.LoggerFactory
+import org.testcontainers.containers.wait.strategy.DockerHealthcheckWaitStrategy
+
+abstract class BaseContainer(val hostName: String) {
+ protected lazy val log = LoggerFacade(LoggerFactory.getLogger(s"${getClass.getSimpleName}.$hostName"))
+
+ protected val container: GenericContainer
+
+ def start(): Unit = {
+ container.start()
+ }
+
+ def waitReady(): Unit = {
+ container.waitingFor(new DockerHealthcheckWaitStrategy)
+ }
+
+ def stop(): Unit = {
+ container.stop()
+ }
+
+ def logPorts(): Unit
+}
diff --git a/consensus-client-it/src/test/scala/units/docker/BesuContainer.scala b/consensus-client-it/src/test/scala/units/docker/BesuContainer.scala
new file mode 100644
index 00000000..61022170
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/docker/BesuContainer.scala
@@ -0,0 +1,52 @@
+package units.docker
+
+import org.testcontainers.containers.BindMode
+import org.testcontainers.containers.Network.NetworkImpl
+import org.web3j.protocol.Web3j
+import org.web3j.protocol.http.HttpService
+import sttp.client3.{Identity, SttpBackend}
+import units.client.JsonRpcClient
+import units.client.engine.{EngineApiClient, HttpEngineApiClient, LoggedEngineApiClient}
+import units.docker.EcContainer.{EnginePort, RpcPort}
+import units.http.OkHttpLogger
+import units.test.TestEnvironment.ConfigsDir
+
+import scala.concurrent.duration.DurationInt
+
+class BesuContainer(network: NetworkImpl, number: Int, ip: String)(implicit httpClientBackend: SttpBackend[Identity, Any])
+ extends EcContainer(number) {
+ protected override val container = new GenericContainer(DockerImages.BesuExecutionClient)
+ .withNetwork(network)
+ .withExposedPorts(RpcPort, EnginePort)
+ .withEnv("LOG4J_CONFIGURATION_FILE", "/config/log4j2.xml")
+ .withEnv("ROOT_LOG_FILE_LEVEL", "TRACE")
+ .withFileSystemBind(s"$ConfigsDir/ec-common/genesis.json", "/genesis.json", BindMode.READ_ONLY)
+ .withFileSystemBind(s"$ConfigsDir/besu", "/config", BindMode.READ_ONLY)
+ .withFileSystemBind(s"$ConfigsDir/besu/run-besu.sh", "/tmp/run.sh", BindMode.READ_ONLY)
+ .withFileSystemBind(s"$ConfigsDir/ec-common/p2p-key-$number.hex", "/etc/secrets/p2p-key", BindMode.READ_ONLY)
+ .withFileSystemBind(s"$logFile", "/opt/besu/logs/besu.log", BindMode.READ_WRITE)
+ .withCreateContainerCmdModifier { cmd =>
+ cmd
+ .withName(s"${network.getName}-$hostName")
+ .withHostName(hostName)
+ .withIpv4Address(ip)
+ .withEntrypoint("/tmp/run.sh")
+ .withStopTimeout(5)
+ }
+
+ override lazy val engineApi: EngineApiClient = new LoggedEngineApiClient(
+ new HttpEngineApiClient(
+ JsonRpcClient.Config(apiUrl = s"http://${container.getHost}:$enginePort", apiRequestRetries = 5, apiRequestRetryWaitTime = 1.second),
+ httpClientBackend
+ )
+ )
+
+ override lazy val web3j = Web3j.build(
+ new HttpService(
+ s"http://${container.getHost}:$rpcPort",
+ HttpService.getOkHttpClientBuilder
+ .addInterceptor(OkHttpLogger)
+ .build()
+ )
+ )
+}
diff --git a/consensus-client-it/src/test/scala/units/docker/DockerImages.scala b/consensus-client-it/src/test/scala/units/docker/DockerImages.scala
new file mode 100644
index 00000000..3806cfa5
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/docker/DockerImages.scala
@@ -0,0 +1,10 @@
+package units.docker
+
+import org.testcontainers.utility.DockerImageName.parse
+import units.test.TestEnvironment.WavesDockerImage
+
+object DockerImages {
+ val WavesNode = parse(WavesDockerImage)
+ val BesuExecutionClient = parse("hyperledger/besu:latest")
+ val GethExecutionClient = parse("ethereum/client-go:stable")
+}
diff --git a/consensus-client-it/src/test/scala/units/docker/EcContainer.scala b/consensus-client-it/src/test/scala/units/docker/EcContainer.scala
new file mode 100644
index 00000000..46b97368
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/docker/EcContainer.scala
@@ -0,0 +1,42 @@
+package units.docker
+
+import com.google.common.io.Files
+import org.web3j.protocol.Web3j
+import units.client.JsonRpcClient
+import units.client.engine.EngineApiClient
+import units.docker.EcContainer.{EnginePort, RpcPort}
+import units.test.TestEnvironment.*
+
+import java.io.File
+import scala.concurrent.duration.DurationInt
+
+abstract class EcContainer(number: Int) extends BaseContainer(s"ec-$number") {
+ protected val logFile = new File(s"$DefaultLogsDir/ec-$number.log")
+ Files.touch(logFile)
+
+ lazy val rpcPort = container.getMappedPort(RpcPort)
+ lazy val enginePort = container.getMappedPort(EnginePort)
+
+ lazy val engineApiDockerUrl = s"http://$hostName:${EcContainer.EnginePort}"
+ lazy val engineApiConfig = JsonRpcClient.Config(
+ apiUrl = s"http://${container.getHost}:$enginePort",
+ apiRequestRetries = 5,
+ apiRequestRetryWaitTime = 1.second
+ )
+
+ def engineApi: EngineApiClient
+ def web3j: Web3j
+
+ override def stop(): Unit = {
+ web3j.shutdown()
+ super.stop()
+ }
+
+ override def logPorts(): Unit = log.debug(s"External host: ${container.getHost}, rpc: $rpcPort, engine: $enginePort")
+}
+
+object EcContainer {
+ val RpcPort = 8545
+ val EnginePort = 8551
+ val ChainId = 1337L // from genesis.json
+}
diff --git a/consensus-client-it/src/test/scala/units/docker/GenericContainer.scala b/consensus-client-it/src/test/scala/units/docker/GenericContainer.scala
new file mode 100644
index 00000000..7704cf02
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/docker/GenericContainer.scala
@@ -0,0 +1,7 @@
+package units.docker
+
+import org.testcontainers.containers.GenericContainer as JGenericContrainer
+import org.testcontainers.utility.DockerImageName
+
+// Fixed autocompletion in IntelliJ IDEA
+class GenericContainer(dockerImageName: DockerImageName) extends JGenericContrainer[GenericContainer](dockerImageName)
diff --git a/consensus-client-it/src/test/scala/units/docker/GethContainer.scala b/consensus-client-it/src/test/scala/units/docker/GethContainer.scala
new file mode 100644
index 00000000..88735ee8
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/docker/GethContainer.scala
@@ -0,0 +1,69 @@
+package units.docker
+
+import okhttp3.Interceptor
+import org.testcontainers.containers.BindMode
+import org.testcontainers.containers.Network.NetworkImpl
+import org.web3j.protocol.Web3j
+import org.web3j.protocol.http.HttpService
+import pdi.jwt.{JwtAlgorithm, JwtClaim, JwtJson}
+import sttp.client3.{Identity, SttpBackend}
+import units.client.JwtAuthenticationBackend
+import units.client.engine.{EngineApiClient, HttpEngineApiClient, LoggedEngineApiClient}
+import units.docker.EcContainer.{EnginePort, RpcPort}
+import units.http.OkHttpLogger
+import units.test.TestEnvironment.ConfigsDir
+
+import java.time.Clock
+import scala.io.Source
+
+class GethContainer(network: NetworkImpl, number: Int, ip: String)(implicit httpClientBackend: SttpBackend[Identity, Any])
+ extends EcContainer(number) {
+ protected override val container = new GenericContainer(DockerImages.GethExecutionClient)
+ .withNetwork(network)
+ .withExposedPorts(RpcPort, EnginePort)
+ .withFileSystemBind(s"$ConfigsDir/ec-common/genesis.json", "/tmp/genesis.json", BindMode.READ_ONLY)
+ .withFileSystemBind(s"$ConfigsDir/geth/run-geth.sh", "/tmp/run.sh", BindMode.READ_ONLY)
+ .withFileSystemBind(s"$ConfigsDir/ec-common/p2p-key-$number.hex", "/etc/secrets/p2p-key", BindMode.READ_ONLY)
+ .withFileSystemBind(s"$ConfigsDir/ec-common/jwt-secret-$number.hex", "/etc/secrets/jwtsecret", BindMode.READ_ONLY)
+ .withFileSystemBind(s"$logFile", "/root/logs/log", BindMode.READ_WRITE)
+ .withCreateContainerCmdModifier { cmd =>
+ cmd
+ .withName(s"${network.getName}-$hostName")
+ .withHostName(hostName)
+ .withIpv4Address(ip)
+ .withEntrypoint("/tmp/run.sh")
+ .withStopTimeout(5)
+ }
+
+ lazy val jwtSecretKey = {
+ val src = Source.fromFile(s"$ConfigsDir/ec-common/jwt-secret-$number.hex")
+ try src.getLines().next()
+ finally src.close()
+ }
+
+ override lazy val engineApi: EngineApiClient = new LoggedEngineApiClient(
+ new HttpEngineApiClient(
+ engineApiConfig,
+ new JwtAuthenticationBackend(jwtSecretKey, httpClientBackend)
+ )
+ )
+
+ override lazy val web3j = Web3j.build(
+ new HttpService(
+ s"http://${container.getHost}:$rpcPort",
+ HttpService.getOkHttpClientBuilder
+ .addInterceptor { (chain: Interceptor.Chain) =>
+ val orig = chain.request()
+ val jwtToken = JwtJson.encode(JwtClaim().issuedNow(Clock.systemUTC), jwtSecretKey, JwtAlgorithm.HS256)
+ val request = orig
+ .newBuilder()
+ .header("Authorization", s"Bearer $jwtToken")
+ .build()
+
+ chain.proceed(request)
+ }
+ .addInterceptor(OkHttpLogger)
+ .build()
+ )
+ )
+}
diff --git a/consensus-client-it/src/test/scala/units/docker/Networks.scala b/consensus-client-it/src/test/scala/units/docker/Networks.scala
new file mode 100644
index 00000000..841e0931
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/docker/Networks.scala
@@ -0,0 +1,28 @@
+package units.docker
+
+import com.github.dockerjava.api.command.CreateNetworkCmd
+import com.github.dockerjava.api.model.Network.Ipam
+import com.google.common.primitives.Ints.toByteArray
+import org.testcontainers.containers.Network
+
+import java.net.InetAddress
+import scala.util.Random
+
+object Networks {
+ // A random network in 10.x.x.x range
+ val networkSeed = Random.nextInt(0x100000) << 4 | 0x0a000000
+
+ // 10.x.x.x/28 network will accommodate up to 13 nodes
+ val networkPrefix = s"${InetAddress.getByAddress(toByteArray(networkSeed)).getHostAddress}/28"
+
+ def network: Network.NetworkImpl = Network
+ .builder()
+ .driver("bridge")
+ .enableIpv6(false)
+ .createNetworkCmdModifier((n: CreateNetworkCmd) =>
+ n.withIpam(new Ipam().withConfig(new Ipam.Config().withSubnet(networkPrefix).withIpRange(networkPrefix)))
+ )
+ .build()
+
+ def ipForNode(nodeId: Int): String = InetAddress.getByAddress(toByteArray(nodeId & 0xf | networkSeed)).getHostAddress
+}
diff --git a/consensus-client-it/src/test/scala/units/docker/WavesNodeContainer.scala b/consensus-client-it/src/test/scala/units/docker/WavesNodeContainer.scala
new file mode 100644
index 00000000..f6af360e
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/docker/WavesNodeContainer.scala
@@ -0,0 +1,104 @@
+package units.docker
+
+import com.google.common.io.Files as GFiles
+import com.google.common.primitives.{Bytes, Ints}
+import com.typesafe.config.ConfigFactory
+import com.wavesplatform.account.{Address, SeedKeyPair}
+import com.wavesplatform.api.NodeHttpApi
+import com.wavesplatform.common.utils.Base58
+import com.wavesplatform.{GenesisBlockGenerator, crypto}
+import org.testcontainers.containers.BindMode
+import org.testcontainers.containers.Network.NetworkImpl
+import sttp.client3.{Identity, SttpBackend, UriContext}
+import units.docker.WavesNodeContainer.*
+import units.test.TestEnvironment.*
+
+import java.io.{File, PrintStream}
+import java.nio.charset.StandardCharsets
+import java.nio.file.{Files, Path}
+import scala.concurrent.duration.DurationInt
+import scala.jdk.CollectionConverters.MapHasAsJava
+import scala.jdk.DurationConverters.JavaDurationOps
+
+class WavesNodeContainer(
+ network: NetworkImpl,
+ number: Int,
+ ip: String,
+ baseSeed: String,
+ chainContractAddress: Address,
+ ecEngineApiUrl: String,
+ genesisConfigPath: Path
+)(implicit httpClientBackend: SttpBackend[Identity, Any])
+ extends BaseContainer(s"wavesnode-$number") {
+ private val logFile = new File(s"$DefaultLogsDir/waves-$number.log")
+ GFiles.touch(logFile)
+
+ protected override val container = new GenericContainer(DockerImages.WavesNode)
+ .withNetwork(network)
+ .withExposedPorts(ApiPort)
+ .withEnv(
+ Map(
+ "NODE_NUMBER" -> s"$number",
+ "WAVES_WALLET_SEED" -> Base58.encode(baseSeed.getBytes(StandardCharsets.UTF_8)),
+ "JAVA_OPTS" -> List(
+ s"-Dunits.defaults.chain-contract=$chainContractAddress",
+ s"-Dunits.defaults.execution-client-address=$ecEngineApiUrl",
+ "-Dlogback.file.level=TRACE",
+ "-Dfile.encoding=UTF-8"
+ ).mkString(" "),
+ "WAVES_LOG_LEVEL" -> "TRACE", // STDOUT logs
+ "WAVES_HEAP_SIZE" -> "1g"
+ ).asJava
+ )
+ .withFileSystemBind(s"$ConfigsDir/wavesnode", "/etc/waves", BindMode.READ_ONLY)
+ .withFileSystemBind(s"$ConfigsDir/ec-common", "/etc/secrets", BindMode.READ_ONLY)
+ .withFileSystemBind(s"$genesisConfigPath", "/etc/it/genesis.conf", BindMode.READ_ONLY)
+ .withFileSystemBind(s"$logFile", "/var/log/waves/waves.log", BindMode.READ_WRITE)
+ .withCreateContainerCmdModifier { cmd =>
+ cmd
+ .withName(s"${network.getName}-$hostName")
+ .withHostName(hostName)
+ .withIpv4Address(ip)
+ .withStopTimeout(5) // Otherwise we don't have logs in the end
+ }
+
+ lazy val apiPort = container.getMappedPort(ApiPort)
+
+ lazy val api = new NodeHttpApi(uri"http://${container.getHost}:$apiPort", httpClientBackend)
+
+ override def logPorts(): Unit = log.debug(s"External host: ${container.getHost}, api: $apiPort")
+}
+
+object WavesNodeContainer {
+ val ApiPort = 6869
+
+ val GenesisTemplateFile = new File(s"$ConfigsDir/wavesnode/genesis-template.conf")
+ val GenesisTemplate = ConfigFactory.parseFile(GenesisTemplateFile)
+ val MaxBlockDelay = GenesisTemplate.getDuration("genesis-generator.average-block-delay").toScala * 2
+
+ def mkKeyPair(seed: String, nonce: Int): SeedKeyPair =
+ SeedKeyPair(crypto.secureHash(Bytes.concat(Ints.toByteArray(nonce), seed.getBytes(StandardCharsets.UTF_8))))
+
+ def generateWavesGenesisConfig(): Path = {
+ val templateFile = ConfigsDir.resolve("wavesnode/genesis-template.conf").toAbsolutePath
+
+ val origConfig = ConfigFactory.parseFile(templateFile.toFile)
+ val gap = 25.seconds // To force node mining at start, otherwise it schedules
+ val overrides = ConfigFactory.parseString(
+ s"""genesis-generator {
+ | timestamp = ${System.currentTimeMillis() - gap.toMillis}
+ |}""".stripMargin
+ )
+
+ val genesisSettings = GenesisBlockGenerator.parseSettings(overrides.withFallback(origConfig))
+
+ val origOut = System.out
+ System.setOut(new PrintStream({ (_: Int) => })) // We don't use System.out in tests, so it should not be an issue
+ val config = GenesisBlockGenerator.createConfig(genesisSettings)
+ System.setOut(origOut)
+
+ val dest = DefaultLogsDir.resolve("genesis.conf").toAbsolutePath
+ Files.writeString(dest, config)
+ dest
+ }
+}
diff --git a/consensus-client-it/src/test/scala/units/el/ElBridgeClient.scala b/consensus-client-it/src/test/scala/units/el/ElBridgeClient.scala
new file mode 100644
index 00000000..a75be3b3
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/el/ElBridgeClient.scala
@@ -0,0 +1,46 @@
+package units.el
+
+import com.wavesplatform.account.Address
+import com.wavesplatform.utils.ScorexLogging
+import org.web3j.abi.datatypes.{AbiTypes, Type}
+import org.web3j.abi.{FunctionReturnDecoder, TypeReference}
+import org.web3j.crypto.Credentials
+import org.web3j.protocol.Web3j
+import org.web3j.protocol.core.methods.response.TransactionReceipt
+import org.web3j.tx.gas.DefaultGasProvider
+import units.bridge.BridgeContract
+import units.eth.EthAddress
+
+import java.util.Collections
+
+class ElBridgeClient(web3j: Web3j, address: EthAddress) extends ScorexLogging {
+ def sendNative(
+ sender: Credentials,
+ recipient: Address,
+ amountInEther: BigInt
+ ): TransactionReceipt = {
+ val senderAddress = sender.getAddress
+ log.debug(s"sendNative($senderAddress->$recipient: $amountInEther Wei)")
+ val bridgeContract = BridgeContract.load(address.hex, web3j, sender, new DefaultGasProvider)
+ bridgeContract.send_sendNative(recipient.publicKeyHash, amountInEther.bigInteger).send()
+ }
+}
+
+object ElBridgeClient {
+ val BurnAddress = EthAddress.unsafeFrom("0x0000000000000000000000000000000000000000")
+
+ def decodeRevertReason(hexRevert: String): String = {
+ val cleanHex = if (hexRevert.startsWith("0x")) hexRevert.drop(2) else hexRevert
+ val errorSignature = "08c379a0" // Error(string)
+
+ if (!cleanHex.startsWith(errorSignature)) throw new RuntimeException(s"Not a revert reason: $hexRevert")
+
+ val strType = TypeReference.create(AbiTypes.getType("string").asInstanceOf[Class[Type[?]]])
+ val revertReasonTypes = Collections.singletonList(strType)
+
+ val encodedReason = "0x" + cleanHex.drop(8)
+ val decoded = FunctionReturnDecoder.decode(encodedReason, revertReasonTypes)
+ if (decoded.isEmpty) throw new RuntimeException(s"Unknown revert reason: $hexRevert")
+ else decoded.get(0).getValue.asInstanceOf[String]
+ }
+}
diff --git a/consensus-client-it/src/test/scala/units/http/OkHttpLogger.scala b/consensus-client-it/src/test/scala/units/http/OkHttpLogger.scala
new file mode 100644
index 00000000..8a2f4cc1
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/http/OkHttpLogger.scala
@@ -0,0 +1,40 @@
+package units.http
+
+import com.wavesplatform.api.LoggingUtil
+import com.wavesplatform.utils.ScorexLogging
+import okhttp3.{Interceptor, Request, Response}
+import play.api.libs.json.Json
+
+import scala.util.Try
+
+object OkHttpLogger extends Interceptor with ScorexLogging {
+ override def intercept(chain: Interceptor.Chain): Response = {
+ val req = chain.request()
+ val bodyStr = readRequestBody(req)
+
+ val currRequestId = (Json.parse(bodyStr) \ "id").asOpt[Long].getOrElse(LoggingUtil.currRequestId.toLong)
+ log.debug(s"[$currRequestId] ${req.method()} ${req.url()}: body=$bodyStr")
+ val res = chain.proceed(req)
+ log.debug(s"[$currRequestId] HTTP ${res.code()}: body=${readResponseBody(res)}")
+ res
+ }
+
+ private def readRequestBody(request: Request): String = request.body() match {
+ case null => "null"
+ case body =>
+ val buffer = new okio.Buffer()
+ Try {
+ body.writeTo(buffer)
+ buffer.readUtf8()
+ }.getOrElse("Could not read body")
+ }
+
+ private def readResponseBody(response: Response): String = response.body() match {
+ case null => "null"
+ case body =>
+ val source = body.source()
+ source.request(Long.MaxValue) // Buffer the entire body.
+ val buffer = source.getBuffer.clone()
+ buffer.readUtf8().trim
+ }
+}
diff --git a/consensus-client-it/src/test/scala/units/test/IntegrationTestEventually.scala b/consensus-client-it/src/test/scala/units/test/IntegrationTestEventually.scala
new file mode 100644
index 00000000..942496e8
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/test/IntegrationTestEventually.scala
@@ -0,0 +1,12 @@
+package units.test
+
+import org.scalatest.concurrent.Eventually
+import org.scalatest.enablers.{ConstantRetrying, Retrying}
+import units.docker.WavesNodeContainer.MaxBlockDelay
+
+import scala.concurrent.duration.DurationInt
+
+trait IntegrationTestEventually extends Eventually {
+ implicit def retrying[T]: Retrying[T] = ConstantRetrying.create[T]
+ implicit override def patienceConfig: PatienceConfig = PatienceConfig(timeout = MaxBlockDelay * 2, interval = 1.second)
+}
diff --git a/consensus-client-it/src/test/scala/units/test/TestEnvironment.scala b/consensus-client-it/src/test/scala/units/test/TestEnvironment.scala
new file mode 100644
index 00000000..f5ef1377
--- /dev/null
+++ b/consensus-client-it/src/test/scala/units/test/TestEnvironment.scala
@@ -0,0 +1,12 @@
+package units.test
+
+import java.nio.file.{Files, Path}
+
+object TestEnvironment {
+ val ConfigsDir: Path = Path.of(System.getProperty("cc.it.configs.dir"))
+ val DefaultLogsDir: Path = Path.of(System.getProperty("cc.it.logs.dir"))
+ Files.createDirectories(DefaultLogsDir)
+
+ val WavesDockerImage: String = System.getProperty("cc.it.docker.image")
+ val ExecutionClient: String = System.getProperty("cc.it.ec", "besu") // | geth
+}
diff --git a/local-network/README.md b/local-network/README.md
index 8f506d9a..8095dd44 100644
--- a/local-network/README.md
+++ b/local-network/README.md
@@ -37,11 +37,7 @@ See [./deploy](./deploy/).
# Keys
* Node HTTP API Key: `testapi`
-* CL accounts (see [genesis-template.conf](configs/wavesnode/genesis-template.conf) for more information):
- * Node wallet seed:
- * wavesnode-1: `devnet-1`
- * wavesnode-2: `devnet-2`
- * Chain contract: `3FdaanzgX4roVgHevhq8L8q42E7EZL9XTQr`
+* CL accounts: see [genesis-template.conf](configs/wavesnode/genesis-template.conf)
* EL mining reward accounts:
* Reward account for **Miner 1** (`wavesnode-1`, `besu-1`):
* Address: `0x7dBcf9c6C3583b76669100f9BE3CaF6d722bc9f9`
diff --git a/local-network/_debug/besu/Dockerfile b/local-network/_debug/besu/Dockerfile
deleted file mode 100644
index c3c297ab..00000000
--- a/local-network/_debug/besu/Dockerfile
+++ /dev/null
@@ -1,10 +0,0 @@
-FROM hyperledger/besu:develop
-
-USER root
-
-RUN apt-get update && apt-get install -y \
- curl \
- netcat \
- iproute2
-
-USER besu
diff --git a/local-network/configs/besu/besu.yml b/local-network/configs/besu/besu.yml
index 95b693d8..e683f057 100644
--- a/local-network/configs/besu/besu.yml
+++ b/local-network/configs/besu/besu.yml
@@ -1,6 +1,6 @@
services:
besu:
- image: hyperledger/besu:latest # Debug version: besu-debug:latest , see _debug/
+ image: hyperledger/besu:latest
volumes:
- ../ec-common/genesis.json:/genesis.json:ro
- .:/config:ro
diff --git a/local-network/configs/besu/log4j2.xml b/local-network/configs/besu/log4j2.xml
index 6309ddbb..17b1a158 100644
--- a/local-network/configs/besu/log4j2.xml
+++ b/local-network/configs/besu/log4j2.xml
@@ -3,18 +3,20 @@
%date %-5level [%-25.25thread] %35.35c{1.} - %msg%n%throwable
+ DEBUG
-
+
-
-
+
+
-
-
+
+
@@ -23,25 +25,25 @@
-
+
-
+
-
+
-
+
-
-
+
+
diff --git a/local-network/configs/besu/run-besu.sh b/local-network/configs/besu/run-besu.sh
index 09c6009e..36c69169 100755
--- a/local-network/configs/besu/run-besu.sh
+++ b/local-network/configs/besu/run-besu.sh
@@ -2,6 +2,10 @@
IP=$(hostname -I)
+tee /opt/besu/logs/besu.log < share
# the sum of shares should be <= initial-balance
distributions = [
- # CL miner on wavesnode-1
+ # Miner on wavesnode-1
# Seed text: devnet-1
# Nonce: 0
# Seed: HnyGuCEnV1A
@@ -17,25 +17,16 @@ genesis-generator {
# Address: 3FNraPMYcfuGREcxorNSEBHgNLjmYtaHy9e
{ seed-text = "devnet-1", nonce = 0, amount = 998036000000000 }
- # Chain contract on wavesnode-1
+ # Miner on wavesnode-1
# Seed text: devnet-1
- # Nonce: 2
+ # Nonce: 1
# Seed: HnyGuCEnV1A
- # Private key: 7TzN4c2XgvSN28gEbvW5wmUecfa4SrPRLXjgr5DRfQMG
- # Public key: 5yxSxDhB2ZL5agYnU6WRtEqPg3T3FfUY3zouKrMpnTX6
- # Address: 3FdaanzgX4roVgHevhq8L8q42E7EZL9XTQr
- { seed-text = "devnet-1", nonce = 2, amount = 20000000000 }
+ # Private key: 5r7KpS3MNXxRcz9jMbpjZKhaZVxDxF74H4VLW6ydi4aT
+ # Public key: 2JYMTjUK7tC8NQi6TD6oWgy41YbrnXuoLzZydrFKTKt6
+ # Address: 3FSgXpgbT6m1speWgVx3cVxAZKmdr4barHU
+ { seed-text = "devnet-1", nonce = 1, amount = 998036000000000 }
- # DAO Address
- # Seed text: devnet dao 1
- # Nonce: 0
- # Seed: 2ttKSSq2o7VU6kFwN
- # Private key: EXC4wafzVus3tmesKEouuewZHGNzEG5qxNHMN66XAiG4
- # Public key: 7S2iCGvRcx3bJA3y8R9k6sJdVATg3ss8r4RuW1S8VriA
- # Address: 3FYp54DX9Np9uouxBtnnCFPYWzz7K3kvSD2
- { seed-text = "devnet dao 1", nonce = 0, amount = 100000000 }
-
- # CL miner on wavesnode-2
+ # Miner on wavesnode-2
# Seed text: devnet-2
# Nonce: 0
# Seed: HnyGuCEnV1B
@@ -46,20 +37,38 @@ genesis-generator {
# Additional addresses
- # Seed text: devnet-0
+ # Seed text: devnet rich
# Nonce: 0
- # Seed: HnyGuCEnV19
- # Private key: 8aYKVCJVYfcSQeSH1kM4D2e4Fcj5cAQzMh8uZ5hW2tB9
- # Public key: GRKksNikCNXYT7HqxCnqzjP5pf4NctgtTnmCvCMDEhAT
- # Address: 3Ffcu52v7ETMAFhNAqQvHsv4JuQjEVP9q6E
- { seed-text = "devnet-0", nonce = 0, amount = 2305842994213693951 }
+ # Seed: RtxP5VZ8GyepM8T
+ # Private key: 5Kn5nRo6piCWg4cwoHWj41L8YnASfYSRt9RkwXxfCjQx
+ # Public key: HWVpTRztoFNwbCsJ1PxtneNSVywkwD8PzMwagDM1YoE6
+ # Address: 3FiaGiTaJ57qLpw5zmdxSejnUYCK2R9E2DV
+ { seed-text = "devnet rich", nonce = 0, amount = 2305842994213693951 }
- # Seed text: devnet-0
+ # Seed text: devnet rich
# Nonce: 1
- # Seed: HnyGuCEnV19
- # Private key: 5tE8jwZ3B4eLTvdz7LqEBMcmspoqXD9nPCEfcZgh2BeV
- # Public key: BArfnBGQ9gkNHC2EftXf42uJuBioSffZiJoyYCTtwxof
- # Address: 3FQFFWxMCDmyZhkRzKA7QiAZL4RKMhJvFnu
- { seed-text = "devnet-0", nonce = 1, amount = 2305842994213693952 }
+ # Seed: RtxP5VZ8GyepM8T
+ # Private key: HkHtYtTVyUy2hcjNLowuEBTrjvCcuq4vGSaWac6Q5gLi
+ # Public key: AhbKXvrNikp2iynszNxAAzoE7hgzaaZpV9jyVpi5D1jS
+ # Address: 3FfXrt2RZCBQm7AKjeRhQjR9vGwu8Cb3RjN
+ { seed-text = "devnet rich", nonce = 1, amount = 2305842994213693952 }
+
+ # DAO Address
+ # Seed text: devnet dao
+ # Nonce: 0
+ # Seed: 6e9UCtnLmrTWeS
+ # Private key: 81JkCduSWu47R9ZsZE6TNdEcaLKPJzxHJAACzKc3qTrV
+ # Public key: 8DTQLSTZ8UcqVRmk7rBeA8vpeHRhcZ2SwtxEuTRRad1x
+ # Address: 3FVp6fUSmVehs7Q3uJBXe5kWtpz57f7azzA
+ { seed-text = "devnet dao", nonce = 0, amount = 100000000 }
+
+ # Chain contract
+ # Seed text: devnet cc
+ # Nonce: 0
+ # Seed: 2H7uPBNbqgJG2
+ # Private key: Ah9hXYB5bPs25iepkAUoR2RGSRB5B7pU4JNM5X6coH1j
+ # Public key: csSdfq8widAC231mR8VQRKcMWWHozZ827kAJWnrLqHR
+ # Address: 3FXDd4LoxxqVLfMk8M25f8CQvfCtGMyiXV1
+ { seed-text = "devnet cc", nonce = 0, amount = 20000000000 }
]
}
diff --git a/local-network/configs/wavesnode/units.conf b/local-network/configs/wavesnode/units.conf
new file mode 100644
index 00000000..0bcbe1e0
--- /dev/null
+++ b/local-network/configs/wavesnode/units.conf
@@ -0,0 +1,23 @@
+units {
+ defaults {
+ chain-contract = "3FXDd4LoxxqVLfMk8M25f8CQvfCtGMyiXV1"
+ execution-client-address = "http://ec-"${NODE_NUMBER}":8551"
+ network {
+ port = 6865
+ bind-address = "0.0.0.0"
+ known-peers = [
+ "wavesnode-1:6865"
+ "wavesnode-2:6865"
+ # Uncomment only if have a waves node. Otherwise, nodes won't connect
+ # "wavesnode-3:6865"
+ # "wavesnode-4:6865"
+ ]
+
+ enable-peers-exchange = no
+ }
+
+ jwt-secret-file = "/etc/secrets/jwt-secret-"${NODE_NUMBER}".hex"
+ }
+
+ chains = [ {} ] # Enable one network
+}
diff --git a/local-network/configs/wavesnode/waves.conf b/local-network/configs/wavesnode/waves.conf
index 6473e406..4acda82f 100644
--- a/local-network/configs/wavesnode/waves.conf
+++ b/local-network/configs/wavesnode/waves.conf
@@ -50,6 +50,7 @@ waves {
voting-interval = 1
}
include "genesis.conf"
+ include "/etc/it/genesis.conf"
}
}
@@ -78,9 +79,11 @@ waves {
known-peers = [
"wavesnode-1:6863"
"wavesnode-2:6863"
+ # Uncomment only if have a waves node. Otherwise, nodes won't connect
+ # "wavesnode-3:6863"
+ # "wavesnode-4:6863"
]
- max-single-host-connections = 6
enable-peers-exchange = no
enable-blacklisting = no
@@ -93,27 +96,6 @@ waves {
}
}
- # P2P Network settings
- l2 {
- chain-contract = "3FdaanzgX4roVgHevhq8L8q42E7EZL9XTQr"
-
- execution-client-address = "http://ec-"${NODE_NUMBER}":8551"
- jwt-secret-file = "/etc/secrets/jwt-secret-"${NODE_NUMBER}".hex"
-
- network {
- bind-address = "0.0.0.0"
- port = 6865
- known-peers = [
- "wavesnode-1:6865"
- "wavesnode-2:6865"
- ]
- enable-peers-exchange = off
- peers-broadcast-interval = 1d
- }
-
- mining-enable = yes
- }
-
rest-api {
# Enable/disable node's REST API
enable = yes
@@ -148,3 +130,5 @@ waves {
password = ""
}
}
+
+include required("units.conf")
diff --git a/local-network/consensus_client-image-build.sh b/local-network/consensus_client-image-build.sh
index 6b52f2db..d79e9518 100755
--- a/local-network/consensus_client-image-build.sh
+++ b/local-network/consensus_client-image-build.sh
@@ -3,5 +3,4 @@
DIR="$(cd "$(dirname "$0")" && pwd)"
cd "${DIR}/.." || exit
-sbt -J-Xmx4G -J-Xss4m -Dfile.encoding=UTF-8 -Dsbt.supershell=false buildTarballsForDocker
-docker build -t consensus-client:local docker
+sbt -J-Xmx4G -J-Xss4m -Dfile.encoding=UTF-8 -Dsbt.supershell=false docker
diff --git a/local-network/deploy/README.md b/local-network/deploy/README.md
index eca81c57..36dfd701 100644
--- a/local-network/deploy/README.md
+++ b/local-network/deploy/README.md
@@ -7,3 +7,15 @@ To run tests on the host machine, from this directory:
1. If you're on macOS with Apple Silicon: install `gcc`.
2. Create the virtual environment and install dependencies: `./dev-setup.sh`
3. Run test, e.g.: `./tests/transfer-c2e.py`
+
+To generate `Bridge.java`, run:
+```bash
+web3j generate solidity \
+ --abiFile=setup/el/compiled/Bridge.abi \
+ --binFile=setup/el/compiled/Bridge.bin \
+ --generateBoth \
+ --outputDir=../../consensus-client-it/src/test/java/ \
+ -p units.bridge \
+ --primitiveTypes \
+ -c BridgeContract
+```
diff --git a/local-network/deploy/deploy.py b/local-network/deploy/deploy.py
index 4e10f9b6..90eef034 100644
--- a/local-network/deploy/deploy.py
+++ b/local-network/deploy/deploy.py
@@ -45,7 +45,7 @@
r = network.cl_chain_contract.setup(
el_genesis_block_hash,
- daoAddress = '3FYp54DX9Np9uouxBtnnCFPYWzz7K3kvSD2'
+ daoAddress = network.cl_dao.address
)
waves.force_success(log, r, "Can not setup the chain contract")
diff --git a/local-network/deploy/local/network.py b/local-network/deploy/local/network.py
index c66e1412..ac808767 100644
--- a/local-network/deploy/local/network.py
+++ b/local-network/deploy/local/network.py
@@ -27,9 +27,13 @@ class Miner:
class ExtendedNetwork(Network):
+ @cached_property
+ def cl_dao(self) -> ChainContract:
+ return pw.Address(seed="devnet dao", nonce=0)
+
@cached_property
def cl_chain_contract(self) -> ChainContract:
- return ChainContract(seed="devnet-1", nonce=2)
+ return ChainContract(seed="devnet cc", nonce=0)
@cached_property
def cl_miners(self) -> List[Miner]:
@@ -56,7 +60,7 @@ def cl_miners(self) -> List[Miner]:
@cached_property
def cl_rich_accounts(self) -> List[pw.Address]:
- return [pw.Address(seed="devnet-0", nonce=n) for n in range(0, 2)]
+ return [pw.Address(seed="devnet rich", nonce=n) for n in range(0, 2)]
@cached_property
def el_rich_accounts(self) -> List[LocalAccount]:
@@ -75,7 +79,7 @@ def el_rich_accounts(self) -> List[LocalAccount]:
chain_id_str="D",
cl_node_api_url=get_waves_api_url(1),
el_node_api_url=get_ec_api_url(1),
- chain_contract_address="3FdaanzgX4roVgHevhq8L8q42E7EZL9XTQr",
+ chain_contract_address="3FXDd4LoxxqVLfMk8M25f8CQvfCtGMyiXV1",
)
diff --git a/local-network/docker-compose.yml b/local-network/docker-compose.yml
index deeca46d..ca880fbe 100644
--- a/local-network/docker-compose.yml
+++ b/local-network/docker-compose.yml
@@ -24,7 +24,7 @@ services:
service: geth
ports:
- "127.0.0.1:28551:8551" # Engine port
- - "127.0.0.1:28545:8545" # RPC port, useful because doesn't require an auth token
+ - "127.0.0.1:28545:8545" # RPC port
volumes:
- ./configs/ec-common/p2p-key-2.hex:/etc/secrets/p2p-key:ro
- ./configs/ec-common/jwt-secret-2.hex:/etc/secrets/jwtsecret:ro
diff --git a/project/plugins.sbt b/project/plugins.sbt
index fb7e7d99..804c55ee 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -3,5 +3,8 @@ resolvers ++= Seq(
Resolver.sbtPluginRepo("releases")
)
-addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.10.0")
-addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12")
+Seq(
+ "com.github.sbt" % "sbt-native-packager" % "1.10.0",
+ "com.github.sbt" % "sbt-ci-release" % "1.5.12",
+ "se.marcuslonnberg" % "sbt-docker" % "1.10.0"
+).map(addSbtPlugin)
diff --git a/src/main/scala/units/Bridge.scala b/src/main/scala/units/Bridge.scala
index 5760258c..2b395a53 100644
--- a/src/main/scala/units/Bridge.scala
+++ b/src/main/scala/units/Bridge.scala
@@ -27,6 +27,9 @@ object Bridge {
val ElSentNativeEventTopic = org.web3j.abi.EventEncoder.encode(ElSentNativeEventDef)
+ /** @param amount
+ * In waves units, see bridge.sol
+ */
case class ElSentNativeEvent(wavesAddress: Address, amount: Long)
object ElSentNativeEvent {
diff --git a/src/main/scala/units/ClientConfig.scala b/src/main/scala/units/ClientConfig.scala
index 82290b7f..b35e5473 100644
--- a/src/main/scala/units/ClientConfig.scala
+++ b/src/main/scala/units/ClientConfig.scala
@@ -6,7 +6,7 @@ import com.wavesplatform.settings.*
import net.ceedubs.ficus.Ficus.*
import net.ceedubs.ficus.readers.ArbitraryTypeReader.arbitraryTypeValueReader
import net.ceedubs.ficus.readers.{Generated, ValueReader}
-import net.ceedubs.ficus.readers.namemappers.implicits.hyphenCase
+import units.client.JsonRpcClient
import scala.concurrent.duration.FiniteDuration
@@ -22,6 +22,12 @@ case class ClientConfig(
jwtSecretFile: Option[String]
) {
lazy val chainContractAddress: Address = Address.fromString(chainContract).explicitGet()
+
+ val jsonRpcClient = JsonRpcClient.Config(
+ apiUrl = executionClientAddress,
+ apiRequestRetries = apiRequestRetries,
+ apiRequestRetryWaitTime = apiRequestRetryWaitTime
+ )
}
object ClientConfig {
diff --git a/src/main/scala/units/ConsensusClientDependencies.scala b/src/main/scala/units/ConsensusClientDependencies.scala
index 1b2a51cf..292c2057 100644
--- a/src/main/scala/units/ConsensusClientDependencies.scala
+++ b/src/main/scala/units/ConsensusClientDependencies.scala
@@ -37,7 +37,7 @@ class ConsensusClientDependencies(val config: ClientConfig) extends AutoCloseabl
httpClientBackend
}
- val engineApiClient = new LoggedEngineApiClient(new HttpEngineApiClient(config, maybeAuthenticatedBackend))
+ val engineApiClient = new LoggedEngineApiClient(new HttpEngineApiClient(config.jsonRpcClient, maybeAuthenticatedBackend))
val allChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE)
val peerDatabase = new PeerDatabaseImpl(config.network)
diff --git a/src/main/scala/units/ELUpdater.scala b/src/main/scala/units/ELUpdater.scala
index 714adf57..e829acab 100644
--- a/src/main/scala/units/ELUpdater.scala
+++ b/src/main/scala/units/ELUpdater.scala
@@ -259,13 +259,14 @@ class ELUpdater(
private def rollbackTo(prevState: Working[ChainStatus], target: L2BlockLike, finalizedBlock: ContractBlock): JobResult[Working[ChainStatus]] = {
val targetHash = target.hash
+ logger.info(s"Starting rollback to $targetHash")
for {
rollbackBlock <- mkRollbackBlock(targetHash)
- _ = logger.info(s"Starting rollback to $targetHash using rollback block ${rollbackBlock.hash}")
+ _ = logger.info(s"Intermediate rollback block: ${rollbackBlock.hash}")
fixedFinalizedBlock = if (finalizedBlock.height > rollbackBlock.parentBlock.height) rollbackBlock.parentBlock else finalizedBlock
_ <- confirmBlock(rollbackBlock.hash, fixedFinalizedBlock.hash)
_ <- confirmBlock(target, fixedFinalizedBlock)
- lastEcBlock <- engineApiClient.getLastExecutionBlock
+ lastEcBlock <- engineApiClient.getLastExecutionBlock()
_ <- Either.cond(
targetHash == lastEcBlock.hash,
(),
@@ -286,7 +287,7 @@ class ELUpdater(
setState("10", newState)
newState
}
- }
+ }.left.map(e => ClientError(s"Error during rollback: ${e.message}"))
private def startBuildingPayload(
epochInfo: EpochInfo,
@@ -454,7 +455,7 @@ class ELUpdater(
(for {
newEpochInfo <- calculateEpochInfo
mainChainInfo <- chainContractClient.getMainChainInfo.toRight("Can't get main chain info")
- lastEcBlock <- engineApiClient.getLastExecutionBlock.leftMap(_.message)
+ lastEcBlock <- engineApiClient.getLastExecutionBlock().leftMap(_.message)
} yield {
logger.trace(s"Following main chain ${mainChainInfo.id}")
val fullValidationStatus = FullValidationStatus(
@@ -890,7 +891,7 @@ class ELUpdater(
private def waitForSyncCompletion(target: ContractBlock): Unit = scheduler.scheduleOnce(5.seconds)(state match {
case SyncingToFinalizedBlock(finalizedBlockHash) if finalizedBlockHash == target.hash =>
logger.debug(s"Checking if EL has synced to ${target.hash} on height ${target.height}")
- engineApiClient.getLastExecutionBlock match {
+ engineApiClient.getLastExecutionBlock() match {
case Left(error) =>
logger.error(s"Sync to ${target.hash} was not completed, error=${error.message}")
setState("23", Starting)
diff --git a/src/main/scala/units/client/JsonRpcClient.scala b/src/main/scala/units/client/JsonRpcClient.scala
index dcc1f99c..d046a9b8 100644
--- a/src/main/scala/units/client/JsonRpcClient.scala
+++ b/src/main/scala/units/client/JsonRpcClient.scala
@@ -2,47 +2,49 @@ package units.client
import cats.Id
import cats.syntax.either.*
-import units.client.JsonRpcClient.DefaultTimeout
-import units.{ClientConfig, ClientError}
+import net.ceedubs.ficus.Ficus.*
+import net.ceedubs.ficus.readers.ArbitraryTypeReader.arbitraryTypeValueReader
+import net.ceedubs.ficus.readers.{Generated, ValueReader}
import play.api.libs.json.{JsError, JsValue, Reads, Writes}
import sttp.client3.*
import sttp.client3.playJson.*
-import sttp.model.Uri
+import units.ClientError
+import units.client.JsonRpcClient.*
import java.util.concurrent.ThreadLocalRandom
-import scala.concurrent.duration.{DurationInt, FiniteDuration}
+import scala.concurrent.duration.FiniteDuration
import scala.util.{Failure, Success, Try}
trait JsonRpcClient {
private type RpcRequest[B] = Request[Either[ResponseException[String, JsError], JsonRpcResponse[B]], Any]
- def config: ClientConfig
+ def config: Config
def backend: SttpBackend[Id, ?]
- def apiUrl: Uri
- protected def sendRequest[RQ: Writes, RP: Reads](requestBody: RQ, timeout: FiniteDuration = DefaultTimeout): Either[String, Option[RP]] =
- sendRequest(mkRequest(requestBody, timeout), config.apiRequestRetries)
+ protected def sendRequest[RQ: Writes, RP: Reads](
+ requestBody: RQ,
+ timeout: FiniteDuration,
+ requestId: Int
+ ): Either[String, Option[RP]] =
+ sendRequest(requestId, mkRequest(requestBody, timeout), config.apiRequestRetries)
protected def parseJson[A: Reads](jsValue: JsValue): Either[ClientError, A] =
Try(jsValue.as[A]).toEither.leftMap(err => ClientError(s"Response parse error: ${err.getMessage}"))
- private def mkRequest[A: Writes, B: Reads](requestBody: A, timeout: FiniteDuration): RpcRequest[B] = {
- val currRequestId = ThreadLocalRandom.current().nextInt(10000, 100000).toString
+ private def mkRequest[A: Writes, B: Reads](requestBody: A, timeout: FiniteDuration): RpcRequest[B] =
basicRequest
.body(requestBody)
- .post(apiUrl)
+ .post(config.sttpApiUri)
.response(asJson[JsonRpcResponse[B]])
.readTimeout(timeout)
- .tag(RequestIdTag, currRequestId)
- }
- private def sendRequest[RQ: Writes, RS: Reads](request: RpcRequest[RS], retriesLeft: Int): Either[String, Option[RS]] = {
+ private def sendRequest[RQ: Writes, RS: Reads](requestId: Int, request: RpcRequest[RS], retriesLeft: Int): Either[String, Option[RS]] = {
def retryIf(cond: Boolean, elseError: String): Either[String, Option[RS]] =
if (cond) {
val retries = retriesLeft - 1
- // TODO: make non-blocking waiting
Thread.sleep(config.apiRequestRetryWaitTime.toMillis)
- sendRequest(request.tag(RetriesLeftTag, retries), retries)
+ onRetry(requestId)
+ sendRequest(requestId, request, retries)
} else Left(elseError)
Try {
@@ -67,8 +69,18 @@ trait JsonRpcClient {
// geth: // https://github.com/ethereum/go-ethereum/blob/master/rpc/errors.go#L71
lcErrorMessage.contains("timed out")
}
+
+ def onRetry(requestId: Int): Unit
}
object JsonRpcClient {
- val DefaultTimeout: FiniteDuration = 1.minute
+ case class Config(apiUrl: String, apiRequestRetries: Int, apiRequestRetryWaitTime: FiniteDuration) {
+ val sttpApiUri = uri"$apiUrl"
+ }
+
+ object Config {
+ implicit val configValueReader: Generated[ValueReader[Config]] = arbitraryTypeValueReader
+ }
+
+ def newRequestId: Int = ThreadLocalRandom.current().nextInt(10000, 100000)
}
diff --git a/src/main/scala/units/client/contract/ChainContractClient.scala b/src/main/scala/units/client/contract/ChainContractClient.scala
index 8c328191..95f0b307 100644
--- a/src/main/scala/units/client/contract/ChainContractClient.scala
+++ b/src/main/scala/units/client/contract/ChainContractClient.scala
@@ -61,7 +61,7 @@ trait ChainContractClient {
val chainHeight = bb.getLong()
val epoch = bb.getLong().toInt // blockMeta is set up in a chain contract and RIDE numbers are Longs
val parentHash = BlockHash(bb.getByteArray(BlockHashBytesSize))
- val chainId = if (bb.remaining() >= 8) bb.getLong() else 0L
+ val chainId = if (bb.remaining() >= 8) bb.getLong() else DefaultMainChainId
val e2cTransfersRootHash =
if (bb.remaining() >= ContractBlock.E2CTransfersRootHashLength) bb.getByteArray(ContractBlock.E2CTransfersRootHashLength)
@@ -228,16 +228,16 @@ trait ChainContractClient {
private def getLastBlockHash(chainId: Long): Option[BlockHash] = getChainMeta(chainId).map(_._2)
- private def getFirstBlockHash(chainId: Long): Option[BlockHash] =
+ protected def getFirstBlockHash(chainId: Long): Option[BlockHash] =
getBlockHash(s"chain${chainId}FirstBlock")
- private def getBinaryData(key: String): Option[ByteStr] =
+ protected def getBinaryData(key: String): Option[ByteStr] =
extractBinaryValue(key, extractData(key))
- private def getStringData(key: String): Option[String] =
+ protected def getStringData(key: String): Option[String] =
extractStringValue(key, extractData(key))
- private def getLongData(key: String): Option[Long] =
+ protected def getLongData(key: String): Option[Long] =
extractLongValue(key, extractData(key))
private def extractLongValue(context: String, extractedDataEntry: Option[DataEntry[?]]): Option[Long] =
@@ -263,7 +263,7 @@ trait ChainContractClient {
object ChainContractClient {
val MinMinerBalance: Long = 20000_00000000L
- val DefaultMainChainId = 0
+ val DefaultMainChainId = 0L
private val AllMinersKey = "allMiners"
private val MainChainIdKey = "mainChainId"
diff --git a/src/main/scala/units/client/engine/EngineApiClient.scala b/src/main/scala/units/client/engine/EngineApiClient.scala
index a8c0a675..b9d8e49b 100644
--- a/src/main/scala/units/client/engine/EngineApiClient.scala
+++ b/src/main/scala/units/client/engine/EngineApiClient.scala
@@ -1,13 +1,14 @@
package units.client.engine
import play.api.libs.json.*
+import units.client.JsonRpcClient.newRequestId
import units.client.engine.EngineApiClient.PayloadId
import units.client.engine.model.*
import units.eth.EthAddress
import units.{BlockHash, JobResult}
trait EngineApiClient {
- def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash): JobResult[PayloadStatus]
+ def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash, requestId: Int = newRequestId): JobResult[PayloadStatus]
def forkChoiceUpdateWithPayloadId(
lastBlockHash: BlockHash,
@@ -15,26 +16,29 @@ trait EngineApiClient {
unixEpochSeconds: Long,
suggestedFeeRecipient: EthAddress,
prevRandao: String,
- withdrawals: Vector[Withdrawal] = Vector.empty
+ withdrawals: Vector[Withdrawal] = Vector.empty,
+ requestId: Int = newRequestId
): JobResult[PayloadId]
- def getPayload(payloadId: PayloadId): JobResult[JsObject]
+ def getPayload(payloadId: PayloadId, requestId: Int = newRequestId): JobResult[JsObject]
- def applyNewPayload(payload: JsObject): JobResult[Option[BlockHash]]
+ def applyNewPayload(payload: JsObject, requestId: Int = newRequestId): JobResult[Option[BlockHash]]
- def getPayloadBodyByHash(hash: BlockHash): JobResult[Option[JsObject]]
+ def getPayloadBodyByHash(hash: BlockHash, requestId: Int = newRequestId): JobResult[Option[JsObject]]
- def getBlockByNumber(number: BlockNumber): JobResult[Option[EcBlock]]
+ def getBlockByNumber(number: BlockNumber, requestId: Int = newRequestId): JobResult[Option[EcBlock]]
- def getBlockByHash(hash: BlockHash): JobResult[Option[EcBlock]]
+ def getBlockByHash(hash: BlockHash, requestId: Int = newRequestId): JobResult[Option[EcBlock]]
- def getBlockByHashJson(hash: BlockHash): JobResult[Option[JsObject]]
+ def getBlockByHashJson(hash: BlockHash, requestId: Int = newRequestId): JobResult[Option[JsObject]]
- def getLastExecutionBlock: JobResult[EcBlock]
+ def getLastExecutionBlock(requestId: Int = newRequestId): JobResult[EcBlock]
- def blockExists(hash: BlockHash): JobResult[Boolean]
+ def blockExists(hash: BlockHash, requestId: Int = newRequestId): JobResult[Boolean]
- def getLogs(hash: BlockHash, address: EthAddress, topic: String): JobResult[List[GetLogsResponseEntry]]
+ def getLogs(hash: BlockHash, address: EthAddress, topic: String, requestId: Int = newRequestId): JobResult[List[GetLogsResponseEntry]]
+
+ def onRetry(requestId: Int): Unit = {}
}
object EngineApiClient {
diff --git a/src/main/scala/units/client/engine/HttpEngineApiClient.scala b/src/main/scala/units/client/engine/HttpEngineApiClient.scala
index 41deab4e..8f6a1671 100644
--- a/src/main/scala/units/client/engine/HttpEngineApiClient.scala
+++ b/src/main/scala/units/client/engine/HttpEngineApiClient.scala
@@ -4,7 +4,6 @@ import cats.syntax.either.*
import cats.syntax.traverse.*
import play.api.libs.json.*
import sttp.client3.*
-import sttp.model.Uri
import units.client.JsonRpcClient
import units.client.engine.EngineApiClient.PayloadId
import units.client.engine.HttpEngineApiClient.*
@@ -12,18 +11,16 @@ import units.client.engine.model.*
import units.client.engine.model.ForkChoiceUpdatedRequest.ForkChoiceAttributes
import units.client.engine.model.PayloadStatus.{Syncing, Valid}
import units.eth.EthAddress
-import units.{BlockHash, ClientConfig, ClientError, JobResult}
+import units.{BlockHash, ClientError, JobResult}
import scala.concurrent.duration.{DurationInt, FiniteDuration}
-class HttpEngineApiClient(val config: ClientConfig, val backend: SttpBackend[Identity, ?]) extends EngineApiClient with JsonRpcClient {
-
- val apiUrl: Uri = uri"${config.executionClientAddress}"
-
- def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash): JobResult[PayloadStatus] = {
+class HttpEngineApiClient(val config: JsonRpcClient.Config, val backend: SttpBackend[Identity, ?]) extends EngineApiClient with JsonRpcClient {
+ def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash, requestId: Int): JobResult[PayloadStatus] = {
sendEngineRequest[ForkChoiceUpdatedRequest, ForkChoiceUpdatedResponse](
- ForkChoiceUpdatedRequest(blockHash, finalizedBlockHash, None),
- BlockExecutionTimeout
+ ForkChoiceUpdatedRequest(blockHash, finalizedBlockHash, None, requestId),
+ BlockExecutionTimeout,
+ requestId
)
.flatMap {
case ForkChoiceUpdatedResponse(ps @ PayloadState(Valid | Syncing, _, _), None) => Right(ps.status)
@@ -39,15 +36,18 @@ class HttpEngineApiClient(val config: ClientConfig, val backend: SttpBackend[Ide
unixEpochSeconds: Long,
suggestedFeeRecipient: EthAddress,
prevRandao: String,
- withdrawals: Vector[Withdrawal]
+ withdrawals: Vector[Withdrawal],
+ requestId: Int
): JobResult[PayloadId] = {
sendEngineRequest[ForkChoiceUpdatedRequest, ForkChoiceUpdatedResponse](
ForkChoiceUpdatedRequest(
lastBlockHash,
finalizedBlockHash,
- Some(ForkChoiceAttributes(unixEpochSeconds, suggestedFeeRecipient, prevRandao, withdrawals))
+ Some(ForkChoiceAttributes(unixEpochSeconds, suggestedFeeRecipient, prevRandao, withdrawals)),
+ requestId
),
- BlockExecutionTimeout
+ BlockExecutionTimeout,
+ requestId
).flatMap {
case ForkChoiceUpdatedResponse(PayloadState(Valid, _, _), Some(payloadId)) =>
Right(payloadId)
@@ -60,12 +60,14 @@ class HttpEngineApiClient(val config: ClientConfig, val backend: SttpBackend[Ide
}
}
- def getPayload(payloadId: PayloadId): JobResult[JsObject] = {
- sendEngineRequest[GetPayloadRequest, GetPayloadResponse](GetPayloadRequest(payloadId), NonBlockExecutionTimeout).map(_.executionPayload)
+ def getPayload(payloadId: PayloadId, requestId: Int): JobResult[JsObject] = {
+ sendEngineRequest[GetPayloadRequest, GetPayloadResponse](GetPayloadRequest(payloadId, requestId), NonBlockExecutionTimeout, requestId).map(
+ _.executionPayload
+ )
}
- def applyNewPayload(payload: JsObject): JobResult[Option[BlockHash]] = {
- sendEngineRequest[NewPayloadRequest, PayloadState](NewPayloadRequest(payload), BlockExecutionTimeout).flatMap {
+ def applyNewPayload(payload: JsObject, requestId: Int): JobResult[Option[BlockHash]] = {
+ sendEngineRequest[NewPayloadRequest, PayloadState](NewPayloadRequest(payload, requestId), BlockExecutionTimeout, requestId).flatMap {
case PayloadState(_, _, Some(validationError)) => Left(ClientError(s"Payload validation error: $validationError"))
case PayloadState(Valid, Some(latestValidHash), _) => Right(Some(latestValidHash))
case PayloadState(Syncing, latestValidHash, _) => Right(latestValidHash)
@@ -74,48 +76,52 @@ class HttpEngineApiClient(val config: ClientConfig, val backend: SttpBackend[Ide
}
}
- def getPayloadBodyByHash(hash: BlockHash): JobResult[Option[JsObject]] = {
- sendEngineRequest[GetPayloadBodyByHash, JsArray](GetPayloadBodyByHash(hash), NonBlockExecutionTimeout)
+ def getPayloadBodyByHash(hash: BlockHash, requestId: Int): JobResult[Option[JsObject]] = {
+ sendEngineRequest[GetPayloadBodyByHash, JsArray](GetPayloadBodyByHash(hash, requestId), NonBlockExecutionTimeout, requestId)
.map(_.value.headOption.flatMap(_.asOpt[JsObject]))
}
- def getBlockByNumber(number: BlockNumber): JobResult[Option[EcBlock]] = {
+ def getBlockByNumber(number: BlockNumber, requestId: Int): JobResult[Option[EcBlock]] = {
for {
- json <- getBlockByNumberJson(number.str)
+ json <- getBlockByNumberJson(number.str, requestId)
blockMeta <- json.traverse(parseJson[EcBlock](_))
} yield blockMeta
}
- def getBlockByHash(hash: BlockHash): JobResult[Option[EcBlock]] = {
- sendRequest[GetBlockByHashRequest, EcBlock](GetBlockByHashRequest(hash))
+ def getBlockByHash(hash: BlockHash, requestId: Int): JobResult[Option[EcBlock]] = {
+ sendRequest[GetBlockByHashRequest, EcBlock](GetBlockByHashRequest(hash, requestId), NonBlockExecutionTimeout, requestId)
.leftMap(err => ClientError(s"Error getting block by hash $hash: $err"))
}
- def getBlockByHashJson(hash: BlockHash): JobResult[Option[JsObject]] = {
- sendRequest[GetBlockByHashRequest, JsObject](GetBlockByHashRequest(hash))
+ def getBlockByHashJson(hash: BlockHash, requestId: Int): JobResult[Option[JsObject]] = {
+ sendRequest[GetBlockByHashRequest, JsObject](GetBlockByHashRequest(hash, requestId), NonBlockExecutionTimeout, requestId)
.leftMap(err => ClientError(s"Error getting block json by hash $hash: $err"))
}
- def getLastExecutionBlock: JobResult[EcBlock] = for {
- lastEcBlockOpt <- getBlockByNumber(BlockNumber.Latest)
+ def getLastExecutionBlock(requestId: Int): JobResult[EcBlock] = for {
+ lastEcBlockOpt <- getBlockByNumber(BlockNumber.Latest, requestId)
lastEcBlock <- Either.fromOption(lastEcBlockOpt, ClientError("Impossible: EC doesn't have blocks"))
} yield lastEcBlock
- def blockExists(hash: BlockHash): JobResult[Boolean] =
- getBlockByHash(hash).map(_.isDefined)
+ def blockExists(hash: BlockHash, requestId: Int): JobResult[Boolean] =
+ getBlockByHash(hash, requestId).map(_.isDefined)
- private def getBlockByNumberJson(number: String): JobResult[Option[JsObject]] = {
- sendRequest[GetBlockByNumberRequest, JsObject](GetBlockByNumberRequest(number))
+ private def getBlockByNumberJson(number: String, requestId: Int): JobResult[Option[JsObject]] = {
+ sendRequest[GetBlockByNumberRequest, JsObject](GetBlockByNumberRequest(number, requestId), NonBlockExecutionTimeout, requestId)
.leftMap(err => ClientError(s"Error getting block by number $number: $err"))
}
- override def getLogs(hash: BlockHash, address: EthAddress, topic: String): JobResult[List[GetLogsResponseEntry]] =
- sendRequest[GetLogsRequest, List[GetLogsResponseEntry]](GetLogsRequest(hash, address, List(topic)))
+ override def getLogs(hash: BlockHash, address: EthAddress, topic: String, requestId: Int): JobResult[List[GetLogsResponseEntry]] =
+ sendRequest[GetLogsRequest, List[GetLogsResponseEntry]](
+ GetLogsRequest(hash, address, List(topic), requestId),
+ NonBlockExecutionTimeout,
+ requestId
+ )
.leftMap(err => ClientError(s"Error getting block logs by hash $hash: $err"))
.map(_.getOrElse(List.empty))
- private def sendEngineRequest[A: Writes, B: Reads](request: A, timeout: FiniteDuration): JobResult[B] = {
- sendRequest(request, timeout) match {
+ private def sendEngineRequest[A: Writes, B: Reads](request: A, timeout: FiniteDuration, requestId: Int): JobResult[B] = {
+ sendRequest(request, timeout, requestId) match {
case Right(response) => response.toRight(ClientError(s"Unexpected engine API empty response"))
case Left(err) => Left(ClientError(s"Engine API request error: $err"))
}
diff --git a/src/main/scala/units/client/engine/LoggedEngineApiClient.scala b/src/main/scala/units/client/engine/LoggedEngineApiClient.scala
index 54a10f64..0cfde809 100644
--- a/src/main/scala/units/client/engine/LoggedEngineApiClient.scala
+++ b/src/main/scala/units/client/engine/LoggedEngineApiClient.scala
@@ -9,14 +9,13 @@ import units.client.engine.model.*
import units.eth.EthAddress
import units.{BlockHash, JobResult}
-import java.util.concurrent.ThreadLocalRandom
import scala.util.chaining.scalaUtilChainingOps
class LoggedEngineApiClient(underlying: EngineApiClient) extends EngineApiClient {
protected val log: LoggerFacade = LoggerFacade(LoggerFactory.getLogger(underlying.getClass))
- override def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash): JobResult[PayloadStatus] =
- wrap(s"forkChoiceUpdate($blockHash, f=$finalizedBlockHash)", underlying.forkChoiceUpdate(blockHash, finalizedBlockHash))
+ override def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash, requestId: Int): JobResult[PayloadStatus] =
+ wrap(requestId, s"forkChoiceUpdate($blockHash, f=$finalizedBlockHash)", underlying.forkChoiceUpdate(blockHash, finalizedBlockHash, _))
override def forkChoiceUpdateWithPayloadId(
lastBlockHash: BlockHash,
@@ -24,50 +23,58 @@ class LoggedEngineApiClient(underlying: EngineApiClient) extends EngineApiClient
unixEpochSeconds: Long,
suggestedFeeRecipient: EthAddress,
prevRandao: String,
- withdrawals: Vector[Withdrawal]
+ withdrawals: Vector[Withdrawal],
+ requestId: Int
): JobResult[PayloadId] = wrap(
+ requestId,
s"forkChoiceUpdateWithPayloadId(l=$lastBlockHash, f=$finalizedBlockHash, ts=$unixEpochSeconds, m=$suggestedFeeRecipient, " +
s"r=$prevRandao, w={${withdrawals.mkString(", ")}}",
- underlying.forkChoiceUpdateWithPayloadId(lastBlockHash, finalizedBlockHash, unixEpochSeconds, suggestedFeeRecipient, prevRandao, withdrawals)
+ underlying.forkChoiceUpdateWithPayloadId(lastBlockHash, finalizedBlockHash, unixEpochSeconds, suggestedFeeRecipient, prevRandao, withdrawals, _)
)
- override def getPayload(payloadId: PayloadId): JobResult[JsObject] =
- wrap(s"getPayload($payloadId)", underlying.getPayload(payloadId), filteredJson)
+ override def getPayload(payloadId: PayloadId, requestId: Int): JobResult[JsObject] =
+ wrap(requestId, s"getPayload($payloadId)", underlying.getPayload(payloadId, _), filteredJson)
- override def applyNewPayload(payload: JsObject): JobResult[Option[BlockHash]] =
- wrap(s"applyNewPayload(${filteredJson(payload)})", underlying.applyNewPayload(payload), _.fold("None")(_.toString))
+ override def applyNewPayload(payload: JsObject, requestId: Int): JobResult[Option[BlockHash]] =
+ wrap(requestId, s"applyNewPayload(${filteredJson(payload)})", underlying.applyNewPayload(payload, _), _.fold("None")(_.toString))
- override def getPayloadBodyByHash(hash: BlockHash): JobResult[Option[JsObject]] =
- wrap(s"getPayloadBodyByHash($hash)", underlying.getPayloadBodyByHash(hash), _.fold("None")(filteredJson))
+ override def getPayloadBodyByHash(hash: BlockHash, requestId: Int): JobResult[Option[JsObject]] =
+ wrap(requestId, s"getPayloadBodyByHash($hash)", underlying.getPayloadBodyByHash(hash, _), _.fold("None")(filteredJson))
- override def getBlockByNumber(number: BlockNumber): JobResult[Option[EcBlock]] =
- wrap(s"getBlockByNumber($number)", underlying.getBlockByNumber(number), _.fold("None")(_.toString))
+ override def getBlockByNumber(number: BlockNumber, requestId: Int): JobResult[Option[EcBlock]] =
+ wrap(requestId, s"getBlockByNumber($number)", underlying.getBlockByNumber(number, _), _.fold("None")(_.toString))
- override def getBlockByHash(hash: BlockHash): JobResult[Option[EcBlock]] =
- wrap(s"getBlockByHash($hash)", underlying.getBlockByHash(hash), _.fold("None")(_.toString))
+ override def getBlockByHash(hash: BlockHash, requestId: Int): JobResult[Option[EcBlock]] =
+ wrap(requestId, s"getBlockByHash($hash)", underlying.getBlockByHash(hash, _), _.fold("None")(_.toString))
- override def getBlockByHashJson(hash: BlockHash): JobResult[Option[JsObject]] =
- wrap(s"getBlockByHashJson($hash)", underlying.getBlockByHashJson(hash), _.fold("None")(filteredJson))
+ override def getBlockByHashJson(hash: BlockHash, requestId: Int): JobResult[Option[JsObject]] =
+ wrap(requestId, s"getBlockByHashJson($hash)", underlying.getBlockByHashJson(hash, _), _.fold("None")(filteredJson))
- override def getLastExecutionBlock: JobResult[EcBlock] =
- wrap("getLastExecutionBlock", underlying.getLastExecutionBlock)
+ override def getLastExecutionBlock(requestId: Int): JobResult[EcBlock] =
+ wrap(requestId, "getLastExecutionBlock", underlying.getLastExecutionBlock)
- override def blockExists(hash: BlockHash): JobResult[Boolean] =
- wrap(s"blockExists($hash)", underlying.blockExists(hash))
+ override def blockExists(hash: BlockHash, requestId: Int): JobResult[Boolean] =
+ wrap(requestId, s"blockExists($hash)", underlying.blockExists(hash, _))
- override def getLogs(hash: BlockHash, address: EthAddress, topic: String): JobResult[List[GetLogsResponseEntry]] =
- wrap(s"getLogs($hash, a=$address, t=$topic)", underlying.getLogs(hash, address, topic), _.view.map(_.data).mkString("{", ", ", "}"))
+ override def getLogs(hash: BlockHash, address: EthAddress, topic: String, requestId: Int): JobResult[List[GetLogsResponseEntry]] =
+ wrap(requestId, s"getLogs($hash, a=$address, t=$topic)", underlying.getLogs(hash, address, topic, _), _.view.map(_.data).mkString("{", ", ", "}"))
- protected def wrap[R](method: String, f: => JobResult[R], toMsg: R => String = (_: R).toString): JobResult[R] = {
- val currRequestId = ThreadLocalRandom.current().nextInt(10000, 100000).toString
- log.debug(s"[$currRequestId] $method")
+ override def onRetry(requestId: Int): Unit = {
+ underlying.onRetry(requestId)
+ logDebug(requestId, "Retry")
+ }
+
+ protected def wrap[R](requestId: Int, method: String, f: Int => JobResult[R], toMsg: R => String = (_: R).toString): JobResult[R] = {
+ logDebug(requestId, method)
- f.tap {
- case Left(e) => log.debug(s"[$currRequestId] Error: ${e.message}")
- case Right(r) => log.debug(s"[$currRequestId] Success: ${toMsg(r)}")
+ f(requestId).tap {
+ case Left(e) => logDebug(requestId, s"Error: ${e.message}")
+ case Right(r) => logDebug(requestId, s"Success: ${toMsg(r)}")
}
}
+ protected def logDebug(requestId: Int, message: String): Unit = log.debug(s"[$requestId] $message")
+
private def filteredJson(jsObject: JsObject): String = JsObject(
jsObject.fields.filterNot { case (k, _) => excludedJsonFields.contains(k) }
).toString()
diff --git a/src/main/scala/units/client/engine/model/ForkChoiceUpdatedRequest.scala b/src/main/scala/units/client/engine/model/ForkChoiceUpdatedRequest.scala
index 742257fe..06f39751 100644
--- a/src/main/scala/units/client/engine/model/ForkChoiceUpdatedRequest.scala
+++ b/src/main/scala/units/client/engine/model/ForkChoiceUpdatedRequest.scala
@@ -1,13 +1,13 @@
package units.client.engine.model
+import play.api.libs.json.*
import units.BlockHash
import units.client.engine.model.ForkChoiceUpdatedRequest.ForkChoiceAttributes
import units.eth.{EthAddress, EthereumConstants}
import units.util.HexBytesConverter.*
-import play.api.libs.json.*
// https://github.com/ethereum/execution-apis/blob/main/src/engine/cancun.md#engine_forkchoiceupdatedv3
-case class ForkChoiceUpdatedRequest(lastBlockHash: BlockHash, finalizedBlockHash: BlockHash, attrs: Option[ForkChoiceAttributes])
+case class ForkChoiceUpdatedRequest(lastBlockHash: BlockHash, finalizedBlockHash: BlockHash, attrs: Option[ForkChoiceAttributes], id: Int)
object ForkChoiceUpdatedRequest {
case class ForkChoiceAttributes(unixEpochSeconds: Long, suggestedFeeRecipient: EthAddress, prevRandao: String, withdrawals: Vector[Withdrawal])
@@ -30,7 +30,7 @@ object ForkChoiceUpdatedRequest {
)
.getOrElse[JsValue](JsNull)
),
- "id" -> 1
+ "id" -> o.id
)
}
}
diff --git a/src/main/scala/units/client/engine/model/GetBlockByHashRequest.scala b/src/main/scala/units/client/engine/model/GetBlockByHashRequest.scala
index c341e4ba..1a65b553 100644
--- a/src/main/scala/units/client/engine/model/GetBlockByHashRequest.scala
+++ b/src/main/scala/units/client/engine/model/GetBlockByHashRequest.scala
@@ -3,14 +3,14 @@ package units.client.engine.model
import play.api.libs.json.{Json, Writes}
import units.BlockHash
-case class GetBlockByHashRequest(hash: BlockHash)
+case class GetBlockByHashRequest(hash: BlockHash, id: Int)
object GetBlockByHashRequest {
implicit val writes: Writes[GetBlockByHashRequest] = (o: GetBlockByHashRequest) => {
Json.obj(
"jsonrpc" -> "2.0",
"method" -> "eth_getBlockByHash",
"params" -> Json.arr(o.hash, false),
- "id" -> 1
+ "id" -> o.id
)
}
}
diff --git a/src/main/scala/units/client/engine/model/GetBlockByNumberRequest.scala b/src/main/scala/units/client/engine/model/GetBlockByNumberRequest.scala
index f3fedd61..c4bbd4e4 100644
--- a/src/main/scala/units/client/engine/model/GetBlockByNumberRequest.scala
+++ b/src/main/scala/units/client/engine/model/GetBlockByNumberRequest.scala
@@ -2,14 +2,14 @@ package units.client.engine.model
import play.api.libs.json.{Json, Writes}
-case class GetBlockByNumberRequest(number: String)
+case class GetBlockByNumberRequest(number: String, id: Int)
object GetBlockByNumberRequest {
implicit val writes: Writes[GetBlockByNumberRequest] = (o: GetBlockByNumberRequest) => {
Json.obj(
"jsonrpc" -> "2.0",
"method" -> "eth_getBlockByNumber",
"params" -> Json.arr(o.number, false),
- "id" -> 1
+ "id" -> o.id
)
}
}
diff --git a/src/main/scala/units/client/engine/model/GetLogsRequest.scala b/src/main/scala/units/client/engine/model/GetLogsRequest.scala
index 1d7fc759..8b37f668 100644
--- a/src/main/scala/units/client/engine/model/GetLogsRequest.scala
+++ b/src/main/scala/units/client/engine/model/GetLogsRequest.scala
@@ -9,7 +9,7 @@ import units.eth.EthAddress
* @see
* https://besu.hyperledger.org/stable/public-networks/reference/api#eth_getlogs
*/
-case class GetLogsRequest(hash: BlockHash, address: EthAddress, topics: List[String])
+case class GetLogsRequest(hash: BlockHash, address: EthAddress, topics: List[String], id: Int)
object GetLogsRequest {
implicit val writes: Writes[GetLogsRequest] = (o: GetLogsRequest) => {
Json.obj(
@@ -22,7 +22,7 @@ object GetLogsRequest {
"topics" -> o.topics
)
),
- "id" -> 1
+ "id" -> o.id
)
}
}
diff --git a/src/main/scala/units/client/engine/model/GetLogsResponseEntry.scala b/src/main/scala/units/client/engine/model/GetLogsResponseEntry.scala
index 0cc0290d..edd4d198 100644
--- a/src/main/scala/units/client/engine/model/GetLogsResponseEntry.scala
+++ b/src/main/scala/units/client/engine/model/GetLogsResponseEntry.scala
@@ -8,8 +8,9 @@ import units.eth.EthAddress
*/
case class GetLogsResponseEntry(
address: EthAddress,
- data: String, // Bytes
- topics: List[String] // TODO type
+ data: String, // Bytes
+ topics: List[String], // TODO type
+ transactionHash: String
)
object GetLogsResponseEntry {
diff --git a/src/main/scala/units/client/engine/model/GetPayloadBodyByHash.scala b/src/main/scala/units/client/engine/model/GetPayloadBodyByHash.scala
index 04b456b1..81cc3607 100644
--- a/src/main/scala/units/client/engine/model/GetPayloadBodyByHash.scala
+++ b/src/main/scala/units/client/engine/model/GetPayloadBodyByHash.scala
@@ -1,9 +1,9 @@
package units.client.engine.model
-import units.BlockHash
import play.api.libs.json.{Json, Writes}
+import units.BlockHash
-case class GetPayloadBodyByHash(hash: BlockHash)
+case class GetPayloadBodyByHash(hash: BlockHash, id: Int)
object GetPayloadBodyByHash {
implicit val writes: Writes[GetPayloadBodyByHash] = (o: GetPayloadBodyByHash) => {
@@ -11,7 +11,7 @@ object GetPayloadBodyByHash {
"jsonrpc" -> "2.0",
"method" -> "engine_getPayloadBodiesByHashV1",
"params" -> Json.arr(Json.arr(o.hash)),
- "id" -> 1
+ "id" -> o.id
)
}
}
diff --git a/src/main/scala/units/client/engine/model/GetPayloadRequest.scala b/src/main/scala/units/client/engine/model/GetPayloadRequest.scala
index 5b7973ca..c6e75e28 100644
--- a/src/main/scala/units/client/engine/model/GetPayloadRequest.scala
+++ b/src/main/scala/units/client/engine/model/GetPayloadRequest.scala
@@ -1,9 +1,9 @@
package units.client.engine.model
-import units.client.engine.EngineApiClient.PayloadId
import play.api.libs.json.{Json, Writes}
+import units.client.engine.EngineApiClient.PayloadId
-case class GetPayloadRequest(payloadId: PayloadId)
+case class GetPayloadRequest(payloadId: PayloadId, id: Int)
object GetPayloadRequest {
implicit val writes: Writes[GetPayloadRequest] = (o: GetPayloadRequest) => {
@@ -11,7 +11,7 @@ object GetPayloadRequest {
"jsonrpc" -> "2.0",
"method" -> "engine_getPayloadV3",
"params" -> Json.arr(o.payloadId),
- "id" -> 1
+ "id" -> o.id
)
}
}
diff --git a/src/main/scala/units/client/engine/model/NewPayloadRequest.scala b/src/main/scala/units/client/engine/model/NewPayloadRequest.scala
index 6b4a0594..d9b25171 100644
--- a/src/main/scala/units/client/engine/model/NewPayloadRequest.scala
+++ b/src/main/scala/units/client/engine/model/NewPayloadRequest.scala
@@ -3,7 +3,7 @@ package units.client.engine.model
import units.eth.EthereumConstants
import play.api.libs.json.{JsObject, Json, Writes}
-case class NewPayloadRequest(payload: JsObject)
+case class NewPayloadRequest(payload: JsObject, id: Int)
object NewPayloadRequest {
implicit val writes: Writes[NewPayloadRequest] = (o: NewPayloadRequest) => {
@@ -15,7 +15,7 @@ object NewPayloadRequest {
Json.arr(),
EthereumConstants.EmptyRootHashHex
),
- "id" -> 1
+ "id" -> o.id
)
}
}
diff --git a/src/main/scala/units/client/package.scala b/src/main/scala/units/client/package.scala
deleted file mode 100644
index c9994e08..00000000
--- a/src/main/scala/units/client/package.scala
+++ /dev/null
@@ -1,6 +0,0 @@
-package units
-
-package object client {
- val RequestIdTag = "requestId"
- val RetriesLeftTag = "retriesLeft"
-}
diff --git a/src/test/resources/application.conf b/src/test/resources/application.conf
index dc1e254c..7bb1c414 100644
--- a/src/test/resources/application.conf
+++ b/src/test/resources/application.conf
@@ -1,13 +1,20 @@
waves {
wallet.seed = "2j8DJP266rEEfdDSfPNbSAqmxMfiyPySHWxCaqP2SHcen8hFbhF1KkqqFH"
- l2 {
+}
+
+units {
+ defaults {
+ chain-contract = "3MsD16zWCbJ4G7QnJizA9x7tM6yDHShoSYB" # Seed: chain-contract
execution-client-address = "http://127.0.0.1:8551"
+ network = ${waves.network}
+
api-request-retries = 2
api-request-retry-wait-time = 2s
block-delay = 6s
block-sync-request-timeout = 500ms
- chain-contract = "3MsD16zWCbJ4G7QnJizA9x7tM6yDHShoSYB" # Seed: chain-contract
+
mining-enable = false
- network = ${waves.network}
}
+
+ chains = [ {} ]
}
diff --git a/src/test/scala/units/BaseIntegrationTestSuite.scala b/src/test/scala/units/BaseIntegrationTestSuite.scala
index e6f07542..4acfd31a 100644
--- a/src/test/scala/units/BaseIntegrationTestSuite.scala
+++ b/src/test/scala/units/BaseIntegrationTestSuite.scala
@@ -36,15 +36,15 @@ trait BaseIntegrationTestSuite
log.debug("EL init")
val txs =
List(
- d.chainContract.setScript(),
- d.chainContract.setup(
+ d.ChainContract.setScript(),
+ d.ChainContract.setup(
d.ecGenesisBlock,
elMinerDefaultReward.amount.longValue(),
defaultSettings.daoRewardAccount.map(_.toAddress),
defaultSettings.daoRewardAmount
)
) ++
- settings.initialMiners.map { x => d.chainContract.join(x.account, x.elRewardAddress) }
+ settings.initialMiners.map { x => d.ChainContract.join(x.account, x.elRewardAddress) }
d.appendBlock(txs*)
d.advanceConsensusLayerChanged()
@@ -108,5 +108,5 @@ trait BaseIntegrationTestSuite
protected def step(name: String): Unit = log.info(s"========= $name =========")
protected def getLogsResponseEntry(event: ElSentNativeEvent): GetLogsResponseEntry =
- GetLogsResponseEntry(elBridgeAddress, Bridge.ElSentNativeEvent.encodeArgs(event), List(Bridge.ElSentNativeEventTopic))
+ GetLogsResponseEntry(elBridgeAddress, Bridge.ElSentNativeEvent.encodeArgs(event), List(Bridge.ElSentNativeEventTopic), "")
}
diff --git a/src/test/scala/units/BlockFullValidationTestSuite.scala b/src/test/scala/units/BlockFullValidationTestSuite.scala
index 643f029c..99c9a941 100644
--- a/src/test/scala/units/BlockFullValidationTestSuite.scala
+++ b/src/test/scala/units/BlockFullValidationTestSuite.scala
@@ -32,7 +32,7 @@ class BlockFullValidationTestSuite extends BaseIntegrationTestSuite {
d.triggerScheduledTasks()
step(s"Append a CL micro block with ecBlock ${ecBlock.hash} confirmation")
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(reliable.account, ecBlock))
d.advanceConsensusLayerChanged()
withClue("Validation doesn't happen:") {
@@ -60,7 +60,7 @@ class BlockFullValidationTestSuite extends BaseIntegrationTestSuite {
d.triggerScheduledTasks()
step(s"Append a CL micro block with ecBlock ${ecBlock.hash} confirmation")
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex))
d.advanceConsensusLayerChanged()
d.waitForCS[FollowingChain]("Following chain") { _ => }
@@ -88,7 +88,7 @@ class BlockFullValidationTestSuite extends BaseIntegrationTestSuite {
val ecBlock1 = d.createEcBlockBuilder("0", malfunction).buildAndSetLogs()
d.ecClients.addKnown(ecBlock1)
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(malfunction.account, ecBlock1))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(malfunction.account, ecBlock1))
d.advanceConsensusLayerChanged()
step("Start new epoch for ecBlock2")
@@ -98,7 +98,7 @@ class BlockFullValidationTestSuite extends BaseIntegrationTestSuite {
val ecBlock2 = badBlockPostProcessing(d.createEcBlockBuilder("0-0", malfunction, ecBlock1).rewardPrevMiner().buildAndSetLogs(blockLogs))
step(s"Append a CL micro block with ecBlock2 ${ecBlock2.hash} confirmation")
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(malfunction.account, ecBlock2, e2CTransfersRootHashHex))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(malfunction.account, ecBlock2, e2CTransfersRootHashHex))
d.advanceConsensusLayerChanged()
step(s"Receive ecBlock2 ${ecBlock2.hash} from a peer")
diff --git a/src/test/scala/units/BlockIssuesForgingTestSuite.scala b/src/test/scala/units/BlockIssuesForgingTestSuite.scala
index 7cea6538..d4322398 100644
--- a/src/test/scala/units/BlockIssuesForgingTestSuite.scala
+++ b/src/test/scala/units/BlockIssuesForgingTestSuite.scala
@@ -5,7 +5,7 @@ import com.wavesplatform.transaction.TxHelpers
import com.wavesplatform.wallet.Wallet
import units.ELUpdater.State.ChainStatus.{FollowingChain, Mining, WaitForNewChain}
import units.ELUpdater.WaitRequestedBlockTimeout
-import units.client.contract.HasConsensusLayerDappTxHelpers.defaultFees
+import units.client.contract.HasConsensusLayerDappTxHelpers.DefaultFees
import units.client.engine.model.EcBlock
import scala.concurrent.duration.DurationInt
@@ -20,7 +20,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite {
override protected val defaultSettings: TestSettings = TestSettings.Default
.copy(
initialMiners = List(thisMiner, otherMiner1, otherMiner2),
- additionalBalances = List(AddrWithBalance(transferReceiver.toAddress, defaultFees.chainContract.withdrawFee))
+ additionalBalances = List(AddrWithBalance(transferReceiver.toAddress, DefaultFees.ChainContract.withdrawFee))
)
.withEnabledElMining
@@ -30,7 +30,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite {
d.advanceNewBlocks(otherMiner1.address)
val ecBlock1 = d.createEcBlockBuilder("0", otherMiner1).buildAndSetLogs()
d.ecClients.addKnown(ecBlock1)
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBlock1))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(otherMiner1.account, ecBlock1))
d.waitForCS[FollowingChain]() { s =>
s.nodeChainInfo.lastBlock.hash shouldBe ecBlock1.hash
}
@@ -39,7 +39,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite {
d.advanceNewBlocks(otherMiner1.address)
val ecBlock2 = d.createEcBlockBuilder("0-0", otherMiner1, ecBlock1).rewardPrevMiner().buildAndSetLogs()
val ecBlock2Epoch = d.blockchain.height
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBlock2))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(otherMiner1.account, ecBlock2))
d.waitForCS[FollowingChain](s"Waiting ecBlock2 ${ecBlock2.hash}") { s =>
s.nodeChainInfo.lastBlock.hash shouldBe ecBlock2.hash
@@ -80,7 +80,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite {
d.advanceNewBlocks(otherMiner1.address)
val ecBlock1 = d.createEcBlockBuilder("0", otherMiner1).buildAndSetLogs()
d.ecClients.addKnown(ecBlock1)
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBlock1))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(otherMiner1.account, ecBlock1))
d.waitForCS[FollowingChain]() { s =>
s.nodeChainInfo.isMain shouldBe true
@@ -90,7 +90,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite {
step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with ecBadBlock2")
d.advanceNewBlocks(otherMiner1.address)
val ecBadBlock2 = d.createEcBlockBuilder("0-0", otherMiner1, ecBlock1).rewardPrevMiner().buildAndSetLogs()
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBadBlock2))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(otherMiner1.account, ecBadBlock2))
d.waitForCS[FollowingChain]() { s =>
s.nodeChainInfo.isMain shouldBe true
@@ -108,7 +108,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite {
s.chainSwitchInfo.referenceBlock.hash shouldBe ecBlock1.hash
}
- d.appendMicroBlockAndVerify(d.chainContract.startAltChain(otherMiner2.account, ecBlock2))
+ d.appendMicroBlockAndVerify(d.ChainContract.startAltChain(otherMiner2.account, ecBlock2))
d.waitForCS[FollowingChain]() { s =>
s.nodeChainInfo.isMain shouldBe false
s.nodeChainInfo.lastBlock.hash shouldBe ecBlock2.hash
@@ -126,7 +126,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite {
d.advanceNewBlocks(otherMiner2.address)
val ecBlock3 = d.createEcBlockBuilder("0-1-1", otherMiner2, parent = ecBlock2).rewardPrevMiner(1).buildAndSetLogs()
val ecBlock3Epoch = d.blockchain.height
- d.appendMicroBlockAndVerify(d.chainContract.extendAltChain(otherMiner2.account, ecBlock3, chainId = 1))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendAltChain(otherMiner2.account, ecBlock3, chainId = 1))
d.waitForCS[FollowingChain]() { s =>
s.nodeChainInfo.isMain shouldBe false
@@ -159,7 +159,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite {
d.advanceNewBlocks(otherMiner1.address)
val ecBlock1 = d.createEcBlockBuilder("0", otherMiner1).buildAndSetLogs()
d.ecClients.addKnown(ecBlock1)
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBlock1))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(otherMiner1.account, ecBlock1))
d.waitForCS[FollowingChain]() { s =>
s.nodeChainInfo.isMain shouldBe true
@@ -169,7 +169,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite {
step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with ecBadBlock2")
d.advanceNewBlocks(otherMiner1.address)
val ecBadBlock2 = d.createEcBlockBuilder("0-0", otherMiner1, ecBlock1).rewardPrevMiner().buildAndSetLogs()
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBadBlock2))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(otherMiner1.account, ecBadBlock2))
d.waitForCS[FollowingChain]() { s =>
s.nodeChainInfo.isMain shouldBe true
@@ -198,7 +198,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite {
step(s"Continue an alternative chain by otherMiner2 ${otherMiner2.address} with ecBadBlock3")
d.advanceNewBlocks(otherMiner2.address)
val ecBadBlock3 = d.createEcBlockBuilder("0-1-1", otherMiner2, ecBlock2).rewardMiner(otherMiner2.elRewardAddress, 1).buildAndSetLogs()
- d.appendMicroBlockAndVerify(d.chainContract.extendAltChain(otherMiner2.account, ecBadBlock3, chainId = 1))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendAltChain(otherMiner2.account, ecBadBlock3, chainId = 1))
d.waitForCS[FollowingChain]() { s =>
s.nodeChainInfo.isMain shouldBe false
@@ -225,7 +225,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite {
d.advanceNewBlocks(otherMiner1.address)
val ecBlock1 = d.createEcBlockBuilder("0", otherMiner1).buildAndSetLogs()
d.ecClients.addKnown(ecBlock1)
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBlock1))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(otherMiner1.account, ecBlock1))
d.waitForCS[FollowingChain]() { s =>
s.nodeChainInfo.isMain shouldBe true
@@ -235,7 +235,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite {
step(s"Start a new epoch of otherMiner1 ${otherMiner1.address} with ecBadBlock2")
d.advanceNewBlocks(otherMiner1.address)
val ecBadBlock2 = d.createEcBlockBuilder("0-0", otherMiner1, ecBlock1).rewardPrevMiner().buildAndSetLogs()
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(otherMiner1.account, ecBadBlock2))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(otherMiner1.account, ecBadBlock2))
d.waitForCS[FollowingChain]() { s =>
s.nodeChainInfo.isMain shouldBe true
@@ -250,7 +250,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite {
s.chainSwitchInfo.referenceBlock.hash shouldBe ecBlock1.hash
}
- d.appendMicroBlockAndVerify(d.chainContract.startAltChain(otherMiner2.account, ecBlock2))
+ d.appendMicroBlockAndVerify(d.ChainContract.startAltChain(otherMiner2.account, ecBlock2))
d.waitForCS[FollowingChain]() { s =>
s.nodeChainInfo.isMain shouldBe false
@@ -268,7 +268,7 @@ class BlockIssuesForgingTestSuite extends BaseIntegrationTestSuite {
step(s"Continue an alternative chain by otherMiner2 ${otherMiner2.address} with ecBlock3")
d.advanceNewBlocks(otherMiner2.address)
val ecBlock3 = d.createEcBlockBuilder("0-1-1", otherMiner2, ecBlock2).rewardPrevMiner(1).buildAndSetLogs()
- d.appendMicroBlockAndVerify(d.chainContract.extendAltChain(otherMiner2.account, ecBlock3, chainId = 1))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendAltChain(otherMiner2.account, ecBlock3, chainId = 1))
d.waitForCS[FollowingChain]() { s =>
s.nodeChainInfo.isMain shouldBe false
diff --git a/src/test/scala/units/C2ETransfersTestSuite.scala b/src/test/scala/units/C2ETransfersTestSuite.scala
index cc9b68dd..5840f43e 100644
--- a/src/test/scala/units/C2ETransfersTestSuite.scala
+++ b/src/test/scala/units/C2ETransfersTestSuite.scala
@@ -68,6 +68,6 @@ class C2ETransfersTestSuite extends BaseIntegrationTestSuite {
)
if (queueSize > 0) d.appendMicroBlock(TxHelpers.data(d.chainContractAccount, Seq(IntegerDataEntry("nativeTransfersCount", queueSize))))
- d.appendMicroBlockE(d.chainContract.transferUnsafe(transferSenderAccount, destElAddressHex, tokenId.getOrElse(d.token), transferAmount))
+ d.appendMicroBlockE(d.ChainContract.transferUnsafe(transferSenderAccount, destElAddressHex, tokenId.getOrElse(d.token), transferAmount))
}
}
diff --git a/src/test/scala/units/E2CTransfersTestSuite.scala b/src/test/scala/units/E2CTransfersTestSuite.scala
index 6881ccf5..cefac22f 100644
--- a/src/test/scala/units/E2CTransfersTestSuite.scala
+++ b/src/test/scala/units/E2CTransfersTestSuite.scala
@@ -11,7 +11,7 @@ import org.web3j.abi.TypeReference
import org.web3j.abi.datatypes.Event
import org.web3j.abi.datatypes.generated.Bytes20
import units.ELUpdater.State.ChainStatus.{Mining, WaitForNewChain}
-import units.client.contract.HasConsensusLayerDappTxHelpers.defaultFees
+import units.client.contract.HasConsensusLayerDappTxHelpers.DefaultFees
import units.eth.EthAddress
import units.util.HexBytesConverter
@@ -30,7 +30,7 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite {
override protected val defaultSettings: TestSettings = TestSettings.Default.copy(
initialMiners = List(reliable),
- additionalBalances = List(AddrWithBalance(transferReceiver.toAddress, defaultFees.chainContract.withdrawFee))
+ additionalBalances = List(AddrWithBalance(transferReceiver.toAddress, DefaultFees.ChainContract.withdrawFee))
)
"Multiple withdrawals" in {
@@ -46,8 +46,8 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite {
val settings = defaultSettings.copy(
additionalBalances = List(
- AddrWithBalance(transferReceiver1.toAddress, defaultFees.chainContract.withdrawFee),
- AddrWithBalance(transferReceiver2.toAddress, defaultFees.chainContract.withdrawFee)
+ AddrWithBalance(transferReceiver1.toAddress, DefaultFees.ChainContract.withdrawFee),
+ AddrWithBalance(transferReceiver2.toAddress, DefaultFees.ChainContract.withdrawFee)
)
)
@@ -57,15 +57,15 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite {
val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs)
def tryWithdraw(): Either[Throwable, BlockId] = d.appendMicroBlockE(
- d.chainContract.withdraw(transferReceiver1, ecBlock, transfer1Proofs, 0, transfer1.amount),
- d.chainContract.withdraw(transferReceiver2, ecBlock, transfer2Proofs, 1, transfer2.amount)
+ d.ChainContract.withdraw(transferReceiver1, ecBlock, transfer1Proofs, 0, transfer1.amount),
+ d.ChainContract.withdraw(transferReceiver2, ecBlock, transfer2Proofs, 1, transfer2.amount)
)
tryWithdraw() should produce("not found for the contract address")
step("Append a CL micro block with ecBlock confirmation")
d.ecClients.addKnown(ecBlock)
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex))
d.advanceConsensusLayerChanged()
tryWithdraw() should beRight
@@ -92,48 +92,44 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite {
d.advanceNewBlocks(reliable.address)
val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs)
d.ecClients.addKnown(ecBlock)
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex))
d.advanceConsensusLayerChanged()
def tryWithdraw(): Either[Throwable, BlockId] =
- d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, ecBlock, transferProofs, index, transfer.amount))
+ d.appendMicroBlockE(d.ChainContract.withdraw(transferReceiver, ecBlock, transferProofs, index, transfer.amount))
tryWithdraw() should produce(errorMessage)
}
}
- "Deny withdrawals with a non-positive amount" in forAll(
- Table(
- "index",
- 0L,
- Long.MinValue
- )
- ) { amount =>
- withExtensionDomain() { d =>
- step(s"Start new epoch with ecBlock")
- d.advanceNewBlocks(reliable.address)
- val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs)
- d.ecClients.addKnown(ecBlock)
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex))
- d.advanceConsensusLayerChanged()
+ private def wrongAmountTest(amount: Long): Unit = withExtensionDomain() { d =>
+ step("Start new epoch with ecBlock")
+ d.advanceNewBlocks(reliable.address)
+ val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs)
+ d.ecClients.addKnown(ecBlock)
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex))
+ d.advanceConsensusLayerChanged()
- def tryWithdraw(): Either[Throwable, BlockId] =
- d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, ecBlock, transferProofs, 0, amount))
+ def tryWithdraw(): Either[Throwable, BlockId] =
+ d.appendMicroBlockE(d.ChainContract.withdraw(transferReceiver, ecBlock, transferProofs, 0, amount))
- tryWithdraw() should produce("Amount should be positive")
- }
+ tryWithdraw() should produce("Amount should be positive")
}
+ "L2-360 Deny negative amount" in wrongAmountTest(Long.MinValue)
+
+ "Deny withdrawals with invalid amount" in forAll(Table("index", 0L, transfer.amount - 1))(wrongAmountTest)
+
"Can't get transferred tokens if the data is incorrect and able if it is correct" in withExtensionDomain() { d =>
step(s"Start new epoch with ecBlock")
d.advanceNewBlocks(reliable.address)
val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs)
def tryWithdraw(): Either[Throwable, BlockId] =
- d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, ecBlock, transferProofs, 0, transfer.amount))
+ d.appendMicroBlockE(d.ChainContract.withdraw(transferReceiver, ecBlock, transferProofs, 0, transfer.amount))
tryWithdraw() should produce("not found for the contract address")
d.ecClients.addKnown(ecBlock)
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex))
d.advanceConsensusLayerChanged()
tryWithdraw() should beRight
@@ -143,9 +139,9 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite {
}
}
- "Can't get transferred tokens twice" in {
+ "L2-273 Can't get transferred tokens twice" in {
val settings = defaultSettings.copy(
- additionalBalances = List(AddrWithBalance(transferReceiver.toAddress, defaultFees.chainContract.withdrawFee * 2))
+ additionalBalances = List(AddrWithBalance(transferReceiver.toAddress, DefaultFees.ChainContract.withdrawFee * 2))
)
withExtensionDomain(settings) { d =>
@@ -153,11 +149,11 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite {
d.advanceNewBlocks(reliable.address)
val ecBlock = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(ecBlockLogs)
def tryWithdraw(): Either[Throwable, BlockId] =
- d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, ecBlock, transferProofs, 0, transfer.amount))
+ d.appendMicroBlockE(d.ChainContract.withdraw(transferReceiver, ecBlock, transferProofs, 0, transfer.amount))
tryWithdraw() should produce("not found for the contract address")
d.ecClients.addKnown(ecBlock)
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(reliable.account, ecBlock, e2CTransfersRootHashHex))
d.advanceConsensusLayerChanged()
tryWithdraw() should beRight
@@ -184,9 +180,9 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite {
withExtensionDomain(settings) { d =>
step(s"Start new epoch with ecBlock1")
d.advanceNewBlocks(reliable.address)
- val ecBlock1 = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(List(transferEvent))
+ val ecBlock1 = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(List(transferEvent))
def tryWithdraw(): Either[Throwable, BlockId] =
- d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, ecBlock1, transferProofs, 0, transfer.amount))
+ d.appendMicroBlockE(d.ChainContract.withdraw(transferReceiver, ecBlock1, transferProofs, 0, transfer.amount))
d.ecClients.willForge(ecBlock1)
d.advanceConsensusLayerChanged()
@@ -209,7 +205,7 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite {
withExtensionDomain(settings) { d =>
step(s"Start new epoch with ecBlock1")
d.advanceNewBlocks(reliable.address)
- val ecBlock1 = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(List(transferEvent.copy(data = "d3ad884fa04292")))
+ val ecBlock1 = d.createEcBlockBuilder("0", reliable).buildAndSetLogs(List(transferEvent.copy(data = "d3ad884fa04292")))
d.ecClients.willForge(ecBlock1)
d.ecClients.willForge(d.createEcBlockBuilder("0-0", reliable).build())
@@ -230,7 +226,7 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite {
d.advanceConsensusLayerChanged()
d.ecClients.addKnown(ecBlock1)
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(malfunction.account, ecBlock1))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(malfunction.account, ecBlock1))
d.advanceConsensusLayerChanged()
step(s"Try to append a block with a wrong transfers root hash")
@@ -239,7 +235,7 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite {
d.advanceConsensusLayerChanged()
// No root hash in extendMainChain tx
- d.appendMicroBlockAndVerify(d.chainContract.extendMainChain(malfunction.account, ecBadBlock2)) // No root hash
+ d.appendMicroBlockAndVerify(d.ChainContract.extendMainChain(malfunction.account, ecBadBlock2)) // No root hash
d.receiveNetworkBlock(ecBadBlock2, malfunction.account)
d.advanceConsensusLayerChanged()
@@ -266,15 +262,15 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite {
step(s"Confirm startAltChain and append with new blocks and remove a malfunction miner")
d.appendMicroBlockAndVerify(
- d.chainContract.startAltChain(reliable.account, ecBlock2, e2CTransfersRootHashHex),
- d.chainContract.leave(malfunction.account)
+ d.ChainContract.startAltChain(reliable.account, ecBlock2, e2CTransfersRootHashHex),
+ d.ChainContract.leave(malfunction.account)
)
d.advanceConsensusLayerChanged()
d.waitForCS[Mining]("State is expected") { _ => }
def tryWithdraw(): Either[Throwable, BlockId] =
- d.appendMicroBlockE(d.chainContract.withdraw(transferReceiver, ecBlock2, transferProofs, 0, transfer.amount))
+ d.appendMicroBlockE(d.ChainContract.withdraw(transferReceiver, ecBlock2, transferProofs, 0, transfer.amount))
withClue("Can't withdraw from a fork:") {
tryWithdraw() should produce("is not finalized")
}
@@ -288,7 +284,7 @@ class E2CTransfersTestSuite extends BaseIntegrationTestSuite {
step("Confirm extendAltChain to make this chain main")
d.advanceMining()
- d.appendMicroBlockAndVerify(d.chainContract.extendAltChain(reliable.account, ecBlock3, chainId = 1))
+ d.appendMicroBlockAndVerify(d.ChainContract.extendAltChain(reliable.account, ecBlock3, chainId = 1))
d.advanceConsensusLayerChanged()
d.waitForCS[Mining]("State is expected") { _ => }
diff --git a/src/test/scala/units/ExtensionDomain.scala b/src/test/scala/units/ExtensionDomain.scala
index a9174658..3c9eeeed 100644
--- a/src/test/scala/units/ExtensionDomain.scala
+++ b/src/test/scala/units/ExtensionDomain.scala
@@ -67,7 +67,7 @@ class ExtensionDomain(
with ScorexLogging { self =>
override val chainContractAccount: KeyPair = KeyPair("chain-contract".getBytes(StandardCharsets.UTF_8))
- val l2Config = settings.config.as[ClientConfig]("waves.l2")
+ val l2Config = settings.config.as[ClientConfig]("units.defaults")
require(l2Config.chainContractAddress == chainContractAddress, "Check settings")
val ecGenesisBlock = EcBlock(
diff --git a/src/test/scala/units/TestSettings.scala b/src/test/scala/units/TestSettings.scala
index 80e3ef52..6aa28955 100644
--- a/src/test/scala/units/TestSettings.scala
+++ b/src/test/scala/units/TestSettings.scala
@@ -29,7 +29,7 @@ object TestSettings {
private object Waves {
val Default = DomainPresets.TransactionStateSnapshot
- val WithMining = Default.copy(config = ConfigFactory.parseString("waves.l2.mining-enable = true").withFallback(Default.config))
+ val WithMining = Default.copy(config = ConfigFactory.parseString("units.defaults.mining-enable = true").withFallback(Default.config))
}
}
diff --git a/src/test/scala/units/client/TestEcClients.scala b/src/test/scala/units/client/TestEcClients.scala
index 53ad627b..0225f1b6 100644
--- a/src/test/scala/units/client/TestEcClients.scala
+++ b/src/test/scala/units/client/TestEcClients.scala
@@ -52,7 +52,7 @@ class TestEcClients private (
private val logs = Atomic(Map.empty[GetLogsRequest, List[GetLogsResponseEntry]])
def setBlockLogs(hash: BlockHash, address: EthAddress, topic: String, blockLogs: List[GetLogsResponseEntry]): Unit = {
- val request = GetLogsRequest(hash, address, List(topic))
+ val request = GetLogsRequest(hash, address, List(topic), 0)
logs.transform(_.updated(request, blockLogs))
}
@@ -64,7 +64,7 @@ class TestEcClients private (
val engineApi = new LoggedEngineApiClient(
new EngineApiClient {
- override def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash): JobResult[PayloadStatus] = {
+ override def forkChoiceUpdate(blockHash: BlockHash, finalizedBlockHash: BlockHash, requestId: Int): JobResult[PayloadStatus] = {
knownBlocks.get().get(blockHash) match {
case Some(cid) =>
currChainIdValue.set(cid)
@@ -85,7 +85,8 @@ class TestEcClients private (
unixEpochSeconds: Long,
suggestedFeeRecipient: EthAddress,
prevRandao: String,
- withdrawals: Vector[Withdrawal]
+ withdrawals: Vector[Withdrawal],
+ requestId: Int
): JobResult[PayloadId] =
forgingBlocks
.get()
@@ -98,7 +99,7 @@ class TestEcClients private (
fb.payloadId.asRight
}
- override def getPayload(payloadId: PayloadId): JobResult[JsObject] =
+ override def getPayload(payloadId: PayloadId, requestId: Int): JobResult[JsObject] =
forgingBlocks.transformAndExtract(_.withoutFirst { fb => fb.payloadId == payloadId }) match {
case Some(fb) => TestEcBlocks.toPayload(fb.testBlock, fb.testBlock.prevRandao).asRight
case None =>
@@ -107,7 +108,7 @@ class TestEcClients private (
)
}
- override def applyNewPayload(payload: JsObject): JobResult[Option[BlockHash]] = {
+ override def applyNewPayload(payload: JsObject, requestId: Int): JobResult[Option[BlockHash]] = {
val newBlock = NetworkL2Block(payload).explicitGet().toEcBlock
knownBlocks.get().get(newBlock.parentHash) match {
case Some(cid) =>
@@ -123,21 +124,21 @@ class TestEcClients private (
knownBlocks.transform(_.updated(newBlock.hash, newCid))
}
- case None => throw notImplementedCase(s"Can't find a parent block ${newBlock.parentHash} for ${newBlock.hash}")
+ case None => notImplementedCase(s"Can't find a parent block ${newBlock.parentHash} for ${newBlock.hash}")
}
Some(newBlock.hash)
}.asRight
- override def getPayloadBodyByHash(hash: BlockHash): JobResult[Option[JsObject]] =
+ override def getPayloadBodyByHash(hash: BlockHash, requestId: Int): JobResult[Option[JsObject]] =
getBlockByHashJson(hash)
- override def getBlockByNumber(number: BlockNumber): JobResult[Option[EcBlock]] =
+ override def getBlockByNumber(number: BlockNumber, requestId: Int): JobResult[Option[EcBlock]] =
number match {
case BlockNumber.Latest => currChain.headOption.asRight
case BlockNumber.Number(n) => currChain.find(_.height == n).asRight
}
- override def getBlockByHash(hash: BlockHash): JobResult[Option[EcBlock]] = {
+ override def getBlockByHash(hash: BlockHash, requestId: Int): JobResult[Option[EcBlock]] = {
for {
cid <- knownBlocks.get().get(hash)
c <- chains.get().get(cid)
@@ -145,23 +146,30 @@ class TestEcClients private (
} yield b
}.asRight
- override def getBlockByHashJson(hash: BlockHash): JobResult[Option[JsObject]] =
+ override def getBlockByHashJson(hash: BlockHash, requestId: Int): JobResult[Option[JsObject]] =
notImplementedMethodJob("getBlockByHashJson")
- override def getLastExecutionBlock: JobResult[EcBlock] = currChain.head.asRight
+ override def getLastExecutionBlock(requestId: Int): JobResult[EcBlock] = currChain.head.asRight
- override def blockExists(hash: BlockHash): JobResult[Boolean] = notImplementedMethodJob("blockExists")
+ override def blockExists(hash: BlockHash, requestId: Int): JobResult[Boolean] = notImplementedMethodJob("blockExists")
- override def getLogs(hash: BlockHash, address: EthAddress, topic: String): JobResult[List[GetLogsResponseEntry]] = {
- val request = GetLogsRequest(hash, address, List(topic))
+ override def getLogs(hash: BlockHash, address: EthAddress, topic: String, requestId: Int): JobResult[List[GetLogsResponseEntry]] = {
+ val request = GetLogsRequest(hash, address, List(topic), 0) // requestId is ignored, see setBlockLogs
getLogsCalls.transform(_ + hash)
- logs.get().getOrElse(request, throw notImplementedCase("call setBlockLogs"))
+ logs.get().getOrElse(request, notImplementedCase("call setBlockLogs"))
}.asRight
}
)
- protected def notImplementedMethodJob[A](text: String): JobResult[A] = throw new NotImplementedMethod(text)
- protected def notImplementedCase(text: String): Throwable = new NotImplementedCase(text)
+ protected def notImplementedMethodJob[A](text: String): JobResult[A] = {
+ log.warn(s"notImplementedMethodJob($text)")
+ throw new NotImplementedMethod(text)
+ }
+
+ protected def notImplementedCase(text: String): Nothing = {
+ log.warn(s"notImplementedCase($text)")
+ throw new NotImplementedCase(text)
+ }
}
object TestEcClients {
diff --git a/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala b/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala
index df79ac99..c0c67fc7 100644
--- a/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala
+++ b/src/test/scala/units/client/contract/HasConsensusLayerDappTxHelpers.scala
@@ -7,11 +7,13 @@ import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.common.utils.EitherExt2
import com.wavesplatform.lang.v1.compiler.Terms
import com.wavesplatform.test.NumericExt
+import com.wavesplatform.transaction.TxHelpers.defaultSigner
import com.wavesplatform.transaction.smart.{InvokeScriptTransaction, SetScriptTransaction}
import com.wavesplatform.transaction.{Asset, TxHelpers}
+import units.BlockHash
import units.client.L2BlockLike
import units.client.contract.HasConsensusLayerDappTxHelpers.*
-import units.client.contract.HasConsensusLayerDappTxHelpers.defaultFees.chainContract.*
+import units.client.contract.HasConsensusLayerDappTxHelpers.DefaultFees.ChainContract.*
import units.eth.{EthAddress, EthereumConstants}
trait HasConsensusLayerDappTxHelpers {
@@ -20,10 +22,16 @@ trait HasConsensusLayerDappTxHelpers {
def chainContractAccount: KeyPair
lazy val chainContractAddress: Address = chainContractAccount.toAddress
- object chainContract {
+ object ChainContract {
def setScript(): SetScriptTransaction = TxHelpers.setScript(chainContractAccount, CompiledChainContract.script, fee = setScriptFee)
- def setup(genesisBlock: L2BlockLike, elMinerReward: Long, daoAddress: Option[Address], daoReward: Long): InvokeScriptTransaction = TxHelpers.invoke(
+ def setup(
+ genesisBlock: L2BlockLike,
+ elMinerReward: Long,
+ daoAddress: Option[Address],
+ daoReward: Long,
+ invoker: KeyPair = defaultSigner
+ ): InvokeScriptTransaction = TxHelpers.invoke(
dApp = chainContractAddress,
func = "setup".some,
args = List(
@@ -32,7 +40,8 @@ trait HasConsensusLayerDappTxHelpers {
Terms.CONST_STRING(daoAddress.fold("")(_.toString)).explicitGet(),
Terms.CONST_LONG(daoReward)
),
- fee = setupFee
+ fee = setupFee,
+ invoker = invoker
)
def join(minerAccount: KeyPair, elRewardAddress: EthAddress): InvokeScriptTransaction = TxHelpers.invoke(
@@ -71,6 +80,28 @@ trait HasConsensusLayerDappTxHelpers {
fee = extendMainChainFee
)
+ def extendMainChain(
+ minerAccount: KeyPair,
+ blockHash: BlockHash,
+ parentBlockHash: BlockHash,
+ e2cTransfersRootHashHex: String,
+ lastC2ETransferIndex: Long,
+ vrf: ByteStr
+ ): InvokeScriptTransaction =
+ TxHelpers.invoke(
+ invoker = minerAccount,
+ dApp = chainContractAddress,
+ func = "extendMainChain".some,
+ args = List(
+ Terms.CONST_STRING(blockHash.drop(2)).explicitGet(),
+ Terms.CONST_STRING(parentBlockHash.drop(2)).explicitGet(),
+ Terms.CONST_BYTESTR(vrf).explicitGet(),
+ Terms.CONST_STRING(e2cTransfersRootHashHex.drop(2)).explicitGet(),
+ Terms.CONST_LONG(lastC2ETransferIndex)
+ ),
+ fee = extendMainChainFee
+ )
+
def appendBlock(
minerAccount: KeyPair,
block: L2BlockLike,
@@ -171,13 +202,21 @@ trait HasConsensusLayerDappTxHelpers {
merkleProof: Seq[Digest],
transferIndexInBlock: Int,
amount: Long
+ ): InvokeScriptTransaction = withdraw(sender, block.hash, merkleProof, transferIndexInBlock, amount)
+
+ def withdraw(
+ sender: KeyPair,
+ blockHash: BlockHash,
+ merkleProof: Seq[Digest],
+ transferIndexInBlock: Int,
+ amount: Long
): InvokeScriptTransaction =
TxHelpers.invoke(
invoker = sender,
dApp = chainContractAddress,
func = "withdraw".some,
args = List(
- Terms.CONST_STRING(block.hash.drop(2)).explicitGet(),
+ Terms.CONST_STRING(blockHash.drop(2)).explicitGet(),
Terms.ARR(merkleProof.map[Terms.EVALUATED](x => Terms.CONST_BYTESTR(ByteStr(x)).explicitGet()).toVector, limited = false).explicitGet(),
Terms.CONST_LONG(transferIndexInBlock),
Terms.CONST_LONG(amount)
@@ -190,8 +229,8 @@ trait HasConsensusLayerDappTxHelpers {
object HasConsensusLayerDappTxHelpers {
val EmptyE2CTransfersRootHashHex = EthereumConstants.NullHex
- object defaultFees {
- object chainContract {
+ object DefaultFees {
+ object ChainContract {
val setScriptFee = 0.05.waves
val setupFee = 2.waves
val joinFee = 0.1.waves