diff --git a/README.adoc b/README.adoc index e54342e..028f3a0 100644 --- a/README.adoc +++ b/README.adoc @@ -1,6 +1,6 @@ = PolkaJ - Polkadot Java Client :lib-version: 0.3.0 -:lib-version-dev: 0.4.0-SNAPSHOT +:lib-version-dev: 0.5.0-SNAPSHOT image:https://github.com/emeraldpay/polkaj/workflows/Tests/badge.svg["Unit Tests"] image:https://codecov.io/gh/emeraldpay/polkaj/branch/master/graph/badge.svg["Coverage",link="https://codecov.io/gh/emeraldpay/polkaj"] @@ -61,7 +61,9 @@ See link:docs/[Documentation] in `./docs` directory, and a demonstration in `./e .Show current finalized block [source,java] ---- -PolkadotHttpApi client = PolkadotHttpApi.newBuilder().build(); +PolkadotHttpApi client = PolkadotApi.newBuilder() + .rpcCallAdapter(JavaHttpAdapter.newBuilder().build()) + .build(); Future hashFuture = client.execute( PolkadotApi.commands().getFinalizedHead() ); diff --git a/build.gradle b/build.gradle index f2532a0..3429a52 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,4 @@ buildscript { - repositories { - maven { url "https://dl.bintray.com/nebula/gradle-plugins" } - } dependencies { classpath 'com.netflix.nebula:gradle-aggregate-javadocs-plugin:3.0.1' @@ -18,7 +15,7 @@ apply plugin: 'jacoco' allprojects { group = 'io.emeraldpay.polkaj' - version = "0.4.0-SNAPSHOT" + version = "0.5.0-SNAPSHOT" repositories { mavenLocal() @@ -28,132 +25,7 @@ allprojects { //for java-multibase maven { url 'https://jitpack.io' } } -} - -subprojects { - apply plugin: 'java' - apply plugin: 'java-library' - apply plugin: 'maven' - apply plugin: 'maven-publish' - apply plugin: 'groovy' - apply plugin: 'jacoco' - apply plugin: 'com.jfrog.bintray' - - targetCompatibility = '11' - sourceCompatibility = '11' - - [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' - - compileJava.options.compilerArgs \ - << '-Xlint:unchecked' << '-Xlint:deprecation' - - dependencies { - testImplementation 'org.objenesis:objenesis:3.1' - testImplementation ('org.spockframework:spock-core:2.0-M2-groovy-3.0') { - exclude group: 'org.codehaus.groovy' - } - testImplementation "nl.jqno.equalsverifier:equalsverifier:3.3" - testImplementation 'org.codehaus.groovy:groovy-all:3.0.3' - testImplementation 'commons-codec:commons-codec:1.14' - testImplementation 'cglib:cglib-nodep:3.3.0' - } - - test { - jvmArgs '-ea' - systemProperty "java.library.path", file("${project.rootDir}/polkaj-schnorrkel/build/rust/release").absolutePath - useJUnitPlatform() - testLogging.showStandardStreams = true - testLogging.exceptionFormat = 'full' - } - - jacocoTestReport { - reports { - xml.enabled true - } - } - - test.finalizedBy jacocoTestReport - - task sourcesJar(type: Jar, dependsOn: classes) { - classifier = 'sources' - from sourceSets.main.allSource - } - task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir - } - - artifacts { - archives sourcesJar - archives javadocJar - } - - jacoco { - toolVersion = "0.8.5" - } - - publishing { - publications { - mavenJava(MavenPublication) { - groupId project.group - artifactId project.name - version project.version - - from components.java - versionMapping { - usage('java-api') { - fromResolutionOf('runtimeClasspath') - } - usage('java-runtime') { - fromResolutionResult() - } - } - pom { - name = 'Polkadot Java Client' - description = 'Java client library to access Polkadot based networks' - licenses { - license { - name = 'The Apache License, Version 2.0' - url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' - } - } - } - } - } - } - - bintray { - user = System.getenv('BINTRAY_USER') - key = System.getenv('BINTRAY_API_KEY') - - dryRun=false - publish=true - override=true - - publications = ['mavenJava'] - - pkg { - userOrg = 'emerald' - repo = 'polkaj' - name = project.name - licenses = ['Apache-2.0'] - vcsUrl = 'https://github.com/emeraldpay/polkaj.git' - labels = ['polkadot', 'blockchain'] - publicDownloadNumbers = true - - version { - name = project.version - description = 'PolkaJ ' + project.version - released = new Date() - vcsTag = project.version - - gpg { - sign = true - } - } - } - } } jacoco { @@ -161,15 +33,11 @@ jacoco { } task coverageReport(type: JacocoReport) { - dependsOn = subprojects.test - additionalSourceDirs.setFrom files(subprojects.sourceSets.main.allSource.srcDirs) - sourceDirectories.setFrom files(subprojects.sourceSets.main.allSource.srcDirs) - classDirectories.setFrom files(subprojects.sourceSets.main.output) executionData.setFrom project.fileTree(dir: '.', include: '**/build/jacoco/test.exec') reports { + html.enabled true xml.enabled true csv.enabled false - html.enabled true } } diff --git a/common_java_app.gradle b/common_java_app.gradle new file mode 100644 index 0000000..5174467 --- /dev/null +++ b/common_java_app.gradle @@ -0,0 +1,145 @@ +apply plugin: 'java' +apply plugin: 'java-library' +apply plugin: 'maven' +apply plugin: 'maven-publish' +apply plugin: 'groovy' +apply plugin: 'jacoco' +apply plugin: 'com.jfrog.bintray' + +afterEvaluate { + rootProject.tasks.coverageReport.additionalSourceDirs.setFrom rootProject.tasks.coverageReport.additionalSourceDirs.files + files(it.sourceSets.main.allSource.srcDirs) + rootProject.tasks.coverageReport.sourceDirectories.setFrom rootProject.tasks.coverageReport.sourceDirectories.files + files(it.sourceSets.main.allSource.srcDirs) + rootProject.tasks.coverageReport.classDirectories.setFrom rootProject.tasks.coverageReport.classDirectories.files + files(it.sourceSets.main.output) +} + + +tasks.withType(JavaCompile) { + options.debug = true +} + +compileJava { + targetCompatibility = '8' + sourceCompatibility = '8' +} + + +[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' + +compileJava.options.compilerArgs \ + << '-Xlint:unchecked' << '-Xlint:deprecation' + +dependencies { + testImplementation 'org.objenesis:objenesis:3.1' + testImplementation ('org.spockframework:spock-core:2.0-M2-groovy-3.0') { + exclude group: 'org.codehaus.groovy' + } + testImplementation "nl.jqno.equalsverifier:equalsverifier:3.3" + testImplementation 'org.codehaus.groovy:groovy-all:3.0.3' + testImplementation 'commons-codec:commons-codec:1.14' + testImplementation 'cglib:cglib-nodep:3.3.0' +} + +test { + jvmArgs '-ea' + systemProperty "java.library.path", file("${project.rootDir}/polkaj-schnorrkel/build/rust/release").absolutePath + useJUnitPlatform() + testLogging.showStandardStreams = true + testLogging.exceptionFormat = 'full' +} + +// Must be after sourceSets closure +jacocoTestReport { + dependsOn tasks.test + + //noinspection GroovyAssignabilityCheck + sourceSets sourceSets.main + reports { + html.enabled = true + xml.enabled = true + csv.enabled = false + } +} +tasks.check.dependsOn tasks.jacocoTestReport +rootProject.tasks.coverageReport.dependsOn tasks.test +test.finalizedBy jacocoTestReport + +task sourcesJar(type: Jar, dependsOn: classes) { + classifier = 'sources' + from sourceSets.main.allSource +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives sourcesJar + archives javadocJar +} + +jacoco { + toolVersion = "0.8.5" +} + +publishing { + publications { + mavenJava(MavenPublication) { + groupId project.group + artifactId project.name + version project.version + + from components.java + versionMapping { + usage('java-api') { + fromResolutionOf('runtimeClasspath') + } + usage('java-runtime') { + fromResolutionResult() + } + } + pom { + name = 'Polkadot Java Client' + description = 'Java client library to access Polkadot based networks' + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + } + } + } +} + +bintray { + user = System.getenv('BINTRAY_USER') + key = System.getenv('BINTRAY_API_KEY') + + dryRun=false + publish=true + override=true + + publications = ['mavenJava'] + + pkg { + userOrg = 'emerald' + repo = 'polkaj' + name = project.name + licenses = ['Apache-2.0'] + vcsUrl = 'https://github.com/emeraldpay/polkaj.git' + labels = ['polkadot', 'blockchain'] + publicDownloadNumbers = true + + version { + name = project.version + description = 'PolkaJ ' + project.version + released = new Date() + vcsTag = project.version + + gpg { + sign = true + } + } + } +} \ No newline at end of file diff --git a/docs/03-rpc-client.adoc b/docs/03-rpc-client.adoc index f97ee06..6c9d3b8 100644 --- a/docs/03-rpc-client.adoc +++ b/docs/03-rpc-client.adoc @@ -113,9 +113,12 @@ dependencies { To create a new client: [source, java] ---- -import io.emeraldpay.polkaj.apihttp.PolkadotHttpApi; +import io.emeraldpay.polkaj.api.PolkadotApi; +import io.emeraldpay.polkaj.apihttp.JavaHttpAdapter; -PolkadotHttpApi client = PolkadotHttpApi.newBuilder().build(); +PolkadotApi api = PolkadotApi.newBuilder() + .rpcCallAdapter(JavaHttpAdapter.newBuilder().build()) + .build(); ---- The default configuration is going to connect to `http://127.0.0.1:9933`, which is a default configuration of a Polkadot node. @@ -123,11 +126,14 @@ If you need to configure the client, follow the builder methods: [source, java] ---- -PolkadotHttpApi client = PolkadotHttpApi.newBuilder() - .objectMapper(objectMapper) // <1> - .connectTo("http://10.0.1.20:9333") // <2> - .basicAuth("alice", "secret") // <3> +PolkadotApi api = PolkadotApi.newBuilder() + .rpcCallAdapter(JavaHttpAdapter.newBuilder() + .rpcCoder(new RpcCoder(objectMapper)) // <1> + .connectTo("http://10.0.1.20:9333") // <2> + .basicAuth("alice", "secret") // <3> + .build()) .build(); + ---- <1> Specify an existing ObjectMapper instance to use by the client <2> Connect to a node at `http://10.0.1.20:9333` @@ -141,6 +147,7 @@ The `PolkadotApi` has the method `execute` that makes an actual call and returns ---- interface PolkadotApi { CompletableFuture execute(RpcCall call); + //... } ---- @@ -258,17 +265,23 @@ It allows subscribing to the events happening on the blockchain, such as changin [source, java] ---- -PolkadotWsApi client = PolkadotWsApi.newBuilder().build(); +JavaHttpSubscriptionAdapter wsAdapter = JavaHttpSubscriptionAdapter.newBuilder().build(); +PolkadotApi api = PolkadotApi.newBuilder() + .subscriptionAdapter(wsAdapter) + .build(); // IMPORTANT! connect to the node as the first step before making calls or subscriptions. -client.connect().get(5, TimeUnit.SECONDS); +wsAdapter.connect().get(5, TimeUnit.SECONDS); ---- -Class `PolkadotWsApi` implements `PolkadotSubscriptionApi` (as well as standard `PolkadotApi`) +Class `JavaHttpSubscriptionAdapter` implements `SubscriptionAdapter` +When setting a `SubscriptionAdapter` via the Builder, it will also be used as the `RpcAdapter` [source, java] ---- -interface PolkadotSubscriptionApi { +interface PolkadotApi { + //... + CompletableFuture> subscribe(SubscribeCall call); } ---- @@ -308,7 +321,7 @@ Note, it's the time we wait for the response from the server that provide use wi [source, java] ---- -Future> hashFuture = client.subscribe( +Future> hashFuture = api.subscribe( PolkadotSubscriptionApi.subscriptions().newHeads() ); Subscription subscription = hashFuture.get(5, TimeUnit.SECONDS); @@ -329,11 +342,11 @@ subscription.handler((Subscription.Event event) -> { }); ---- -Since the `PolkadotWsApi` implements standard `PolkadotApi` you can make all other calls through the same WebSocket connection: +Since the `SubscriptionAdapter` extends standard `RpcAdapter` you can make all other calls through the same WebSocket connection: [source, java] ---- -Future previousBlock = client.execute( +Future previousBlock = api.execute( PolkadotApi.commands().getBlock(header.getParentHash()) ); ---- diff --git a/docs/06-balance.adoc b/docs/06-balance.adoc index 51a5538..d05b5ad 100644 --- a/docs/06-balance.adoc +++ b/docs/06-balance.adoc @@ -10,11 +10,13 @@ For the query it encodes request to storage function `TotalIssuance` of the modu [source, java] ---- +final JavaHttpSubscriptionAdapter adapter = JavaHttpSubscriptionAdapter .newBuilder() + .connectTo("wss://cc3-5.kusama.network") + .build(); try ( - PolkadotHttpApi client = PolkadotWsApi.newBuilder() - .connectTo("wss://cc3-5.kusama.network") - .build() -) { + PolkadotApi client = PolkadotApi.newBuilder().subscriptionAdapter(adapter).build()) +).build()) { + System.out.println("Connected: " + adapter.connect().get()); DotAmount total = AccountRequests.totalIssuance() // execute on RPC .execute(client) @@ -35,7 +37,8 @@ For the query it encodes request to storage function `System` of the module `Acc [source, java] ---- try ( - PolkadotHttpApi client = PolkadotWsApi.newBuilder() + PolkadotApi client =PolkadotApi.newBuilder().rpcCallAdapter( + JavaHttpAdapter.newBuilder() .connectTo("wss://cc3-5.kusama.network") .build() ) { @@ -69,7 +72,8 @@ To make a valid extrinsic, it also needs the current Runtime Metadata and Extrin [source, java] ---- try ( - PolkadotHttpApi client = PolkadotWsApi.newBuilder() + PolkadotApi client =PolkadotApi.newBuilder().rpcCallAdapter( + JavaHttpAdapter.newBuilder() .connectTo("wss://cc3-5.kusama.network") .build() ) { diff --git a/examples/README.adoc b/examples/README.adoc index ac3ae19..67c8dfc 100644 --- a/examples/README.adoc +++ b/examples/README.adoc @@ -84,4 +84,4 @@ cd balance ./gradlew run --args="wss://cc3-5.kusama.network SENDER_KEY_SEED RECIPIENT_ADDRESS" ---- -WARNING: If you use on a real network please be aware that it executes the transfer without any additional convirmation from you \ No newline at end of file +WARNING: If you use on a real network please be aware that it executes the transfer without any additional confirmation from you \ No newline at end of file diff --git a/examples/balance/gradle/wrapper/gradle-wrapper.properties b/examples/balance/gradle/wrapper/gradle-wrapper.properties index ba94df8..da9702f 100644 --- a/examples/balance/gradle/wrapper/gradle-wrapper.properties +++ b/examples/balance/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/balance/src/main/java/Balance.java b/examples/balance/src/main/java/Balance.java index b05a6a7..fe18618 100644 --- a/examples/balance/src/main/java/Balance.java +++ b/examples/balance/src/main/java/Balance.java @@ -1,4 +1,5 @@ -import io.emeraldpay.polkaj.apihttp.PolkadotHttpApi; +import io.emeraldpay.polkaj.api.PolkadotApi; +import io.emeraldpay.polkaj.apihttp.JavaHttpAdapter; import io.emeraldpay.polkaj.scaletypes.AccountInfo; import io.emeraldpay.polkaj.tx.AccountRequests; import io.emeraldpay.polkaj.types.Address; @@ -8,7 +9,7 @@ public class Balance { public static void main(String[] args) throws Exception { - try (PolkadotHttpApi client = PolkadotHttpApi.newBuilder().build()) { + try (PolkadotApi client = PolkadotApi.newBuilder().rpcCallAdapter(JavaHttpAdapter.newBuilder().build()).build()) { DotAmountFormatter formatter = DotAmountFormatter.autoFormatter(); DotAmount total = AccountRequests.totalIssuance().execute(client).get(); diff --git a/examples/balance/src/main/java/Transfer.java b/examples/balance/src/main/java/Transfer.java index 17f44a6..dc74d0d 100644 --- a/examples/balance/src/main/java/Transfer.java +++ b/examples/balance/src/main/java/Transfer.java @@ -1,6 +1,5 @@ import io.emeraldpay.polkaj.api.*; -import io.emeraldpay.polkaj.apiws.PolkadotWsApi; -import io.emeraldpay.polkaj.json.RuntimeVersionJson; +import io.emeraldpay.polkaj.apiws.JavaHttpSubscriptionAdapter; import io.emeraldpay.polkaj.scale.ScaleExtract; import io.emeraldpay.polkaj.scaletypes.AccountInfo; import io.emeraldpay.polkaj.scaletypes.Metadata; @@ -50,8 +49,9 @@ public static void main(String[] args) throws Exception { Math.abs(random.nextLong()) % DotAmount.fromDots(0.002).getValue().longValue() ); - try (PolkadotWsApi client = PolkadotWsApi.newBuilder().connectTo(api).build()) { - System.out.println("Connected: " + client.connect().get()); + final JavaHttpSubscriptionAdapter adapter = JavaHttpSubscriptionAdapter.newBuilder().connectTo(api).build(); + try (PolkadotApi client = PolkadotApi.newBuilder().subscriptionAdapter(adapter).build()) { + System.out.println("Connected: " + adapter.connect().get()); // Subscribe to block heights AtomicLong height = new AtomicLong(0); diff --git a/examples/common.gradle b/examples/common.gradle index 48f909c..606b1c3 100644 --- a/examples/common.gradle +++ b/examples/common.gradle @@ -2,7 +2,7 @@ // Common config for Polkaj Examples // ext { - polkajVersion = "0.4.0-SNAPSHOT" + polkajVersion = "0.5.0-SNAPSHOT" } repositories { diff --git a/examples/encoding/gradle/wrapper/gradle-wrapper.properties b/examples/encoding/gradle/wrapper/gradle-wrapper.properties index ba94df8..da9702f 100644 --- a/examples/encoding/gradle/wrapper/gradle-wrapper.properties +++ b/examples/encoding/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/keys/gradle/wrapper/gradle-wrapper.properties b/examples/keys/gradle/wrapper/gradle-wrapper.properties index 1b16c34..9938269 100644 --- a/examples/keys/gradle/wrapper/gradle-wrapper.properties +++ b/examples/keys/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/examples/rpc/gradle/wrapper/gradle-wrapper.properties b/examples/rpc/gradle/wrapper/gradle-wrapper.properties index ba94df8..da9702f 100644 --- a/examples/rpc/gradle/wrapper/gradle-wrapper.properties +++ b/examples/rpc/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/rpc/src/main/java/DescribeRuntime.java b/examples/rpc/src/main/java/DescribeRuntime.java index 6a7c5da..a038328 100644 --- a/examples/rpc/src/main/java/DescribeRuntime.java +++ b/examples/rpc/src/main/java/DescribeRuntime.java @@ -1,9 +1,9 @@ +import io.emeraldpay.polkaj.api.PolkadotApi; import io.emeraldpay.polkaj.api.StandardCommands; -import io.emeraldpay.polkaj.apihttp.PolkadotHttpApi; +import io.emeraldpay.polkaj.apihttp.JavaHttpAdapter; import io.emeraldpay.polkaj.scale.ScaleExtract; import io.emeraldpay.polkaj.scaletypes.Metadata; import io.emeraldpay.polkaj.scaletypes.MetadataReader; -import io.emeraldpay.polkaj.types.ByteData; import java.util.concurrent.Future; @@ -13,8 +13,10 @@ public class DescribeRuntime { public static void main(String[] args) throws Exception { - PolkadotHttpApi client = PolkadotHttpApi.newBuilder().build(); - Future metadataFuture = client.execute(StandardCommands.getInstance().stateMetadata()) + PolkadotApi api = PolkadotApi.newBuilder() + .rpcCallAdapter(JavaHttpAdapter.newBuilder().build()) + .build(); + Future metadataFuture = api.execute(StandardCommands.getInstance().stateMetadata()) .thenApply(ScaleExtract.fromBytesData(new MetadataReader())); System.out.println("Runtime Metadata:"); @@ -36,6 +38,6 @@ public static void main(String[] args) throws Exception { }); } }); - client.close(); + api.close(); } } diff --git a/examples/rpc/src/main/java/FollowState.java b/examples/rpc/src/main/java/FollowState.java index c9cbe98..877ffa1 100644 --- a/examples/rpc/src/main/java/FollowState.java +++ b/examples/rpc/src/main/java/FollowState.java @@ -1,12 +1,11 @@ -import io.emeraldpay.polkaj.api.PolkadotSubscriptionApi; -import io.emeraldpay.polkaj.api.SubscribeCall; +import io.emeraldpay.polkaj.api.PolkadotApi; import io.emeraldpay.polkaj.api.Subscription; -import io.emeraldpay.polkaj.apiws.PolkadotWsApi; +import io.emeraldpay.polkaj.api.SubscriptionAdapter; +import io.emeraldpay.polkaj.apiws.JavaHttpSubscriptionAdapter; import io.emeraldpay.polkaj.json.BlockJson; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -16,12 +15,15 @@ public class FollowState { public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException { - PolkadotWsApi client = PolkadotWsApi.newBuilder().build(); + JavaHttpSubscriptionAdapter wsAdapter = JavaHttpSubscriptionAdapter.newBuilder().build(); + PolkadotApi api = PolkadotApi.newBuilder() + .subscriptionAdapter(wsAdapter) + .build(); // IMPORTANT! connect to the node as the first step before making calls or subscriptions. - client.connect().get(5, TimeUnit.SECONDS); + wsAdapter.connect().get(5, TimeUnit.SECONDS); - Future> hashFuture = client.subscribe(PolkadotSubscriptionApi.subscriptions().newHeads()); + Future> hashFuture = api.subscribe(SubscriptionAdapter.subscriptions().newHeads()); Subscription subscription = hashFuture.get(5, TimeUnit.SECONDS); subscription.handler((Subscription.Event event) -> { diff --git a/examples/rpc/src/main/java/ShowState.java b/examples/rpc/src/main/java/ShowState.java index 05243cf..1a72fef 100644 --- a/examples/rpc/src/main/java/ShowState.java +++ b/examples/rpc/src/main/java/ShowState.java @@ -1,23 +1,23 @@ -import com.fasterxml.jackson.databind.ObjectMapper; import io.emeraldpay.polkaj.api.PolkadotApi; import io.emeraldpay.polkaj.api.PolkadotMethod; import io.emeraldpay.polkaj.api.RpcCall; -import io.emeraldpay.polkaj.apihttp.PolkadotHttpApi; +import io.emeraldpay.polkaj.apihttp.JavaHttpAdapter; import io.emeraldpay.polkaj.json.BlockResponseJson; import io.emeraldpay.polkaj.json.RuntimeVersionJson; import io.emeraldpay.polkaj.json.SystemHealthJson; -import io.emeraldpay.polkaj.json.jackson.PolkadotModule; import io.emeraldpay.polkaj.types.Hash256; -import java.net.URISyntaxException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; public class ShowState { public static void main(String[] args) throws Exception { - PolkadotHttpApi client = PolkadotHttpApi.newBuilder().build(); - Future hashFuture = client.execute( + PolkadotApi api = PolkadotApi.newBuilder() + .rpcCallAdapter(JavaHttpAdapter.newBuilder().build()) + .build(); + + Future hashFuture = api.execute( // use RpcCall.create to define the request // the first parameter is Class / JavaType of the expected result // second is the method name @@ -26,9 +26,9 @@ public static void main(String[] args) throws Exception { ); Hash256 hash = hashFuture.get(); - Hash256 blockHash = client.execute(PolkadotApi.commands().getBlockHash()).get(); + Hash256 blockHash = api.execute(PolkadotApi.commands().getBlockHash()).get(); - Future blockFuture = client.execute( + Future blockFuture = api.execute( // Another way to prepare a call, instead of manually constructing RpcCall instances // is to use standard commands provided by PolkadotApi.commands() // the following line is same as calling with @@ -37,13 +37,13 @@ public static void main(String[] args) throws Exception { ); BlockResponseJson block = blockFuture.get(); - String version = client.execute(PolkadotApi.commands().systemVersion()) + String version = api.execute(PolkadotApi.commands().systemVersion()) .get(5, TimeUnit.SECONDS); - RuntimeVersionJson runtimeVersion = client.execute(PolkadotApi.commands().getRuntimeVersion()) + RuntimeVersionJson runtimeVersion = api.execute(PolkadotApi.commands().getRuntimeVersion()) .get(5, TimeUnit.SECONDS); - SystemHealthJson health = client.execute(PolkadotApi.commands().systemHealth()) + SystemHealthJson health = api.execute(PolkadotApi.commands().systemHealth()) .get(5, TimeUnit.SECONDS); System.out.println("Software: " + version); @@ -55,20 +55,20 @@ public static void main(String[] args) throws Exception { System.out.println("Current block hash: " + blockHash); System.out.println("Current height: " + block.getBlock().getHeader().getNumber()); System.out.println("State hash: " + block.getBlock().getHeader().getStateRoot()); - client.close(); + api.close(); } - PolkadotApi client() throws URISyntaxException { - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.registerModule(new PolkadotModule()); - - PolkadotHttpApi client = PolkadotHttpApi.newBuilder() - .objectMapper(objectMapper) // <1> - .connectTo("http://10.0.1.20:9333") // <2> - .basicAuth("alice", "secret") // <3> - .build(); - - return client; - } +// PolkadotApi client() throws URISyntaxException { +// ObjectMapper objectMapper = new ObjectMapper(); +// objectMapper.registerModule(new PolkadotModule()); +// +// PolkadotHttpApi client = PolkadotHttpApi.newBuilder() +// .objectMapper(objectMapper) // <1> +// .connectTo("http://10.0.1.20:9333") // <2> +// .basicAuth("alice", "secret") // <3> +// .build(); +// +// return client; +// } } diff --git a/examples/runtime-explorer/gradle/wrapper/gradle-wrapper.properties b/examples/runtime-explorer/gradle/wrapper/gradle-wrapper.properties index ba94df8..da9702f 100644 --- a/examples/runtime-explorer/gradle/wrapper/gradle-wrapper.properties +++ b/examples/runtime-explorer/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/runtime-explorer/src/main/java/example/rtweb/service/MetadataService.java b/examples/runtime-explorer/src/main/java/example/rtweb/service/MetadataService.java index 491b702..d715b26 100644 --- a/examples/runtime-explorer/src/main/java/example/rtweb/service/MetadataService.java +++ b/examples/runtime-explorer/src/main/java/example/rtweb/service/MetadataService.java @@ -2,7 +2,7 @@ import io.emeraldpay.polkaj.api.PolkadotApi; import io.emeraldpay.polkaj.api.StandardCommands; -import io.emeraldpay.polkaj.apihttp.PolkadotHttpApi; +import io.emeraldpay.polkaj.apihttp.JavaHttpAdapter; import io.emeraldpay.polkaj.scale.ScaleExtract; import io.emeraldpay.polkaj.scaletypes.Metadata; import io.emeraldpay.polkaj.scaletypes.MetadataReader; @@ -18,7 +18,9 @@ public class MetadataService { @PostConstruct public void init() { - api = PolkadotHttpApi.newBuilder().build(); + api = PolkadotApi.newBuilder() + .rpcCallAdapter(JavaHttpAdapter.newBuilder().build()) + .build(); } public Metadata get() { diff --git a/examples/types/gradle/wrapper/gradle-wrapper.properties b/examples/types/gradle/wrapper/gradle-wrapper.properties index ba94df8..da9702f 100644 --- a/examples/types/gradle/wrapper/gradle-wrapper.properties +++ b/examples/types/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a4b4429..da9702f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/polkaj-adapter-tests/build.gradle b/polkaj-adapter-tests/build.gradle new file mode 100644 index 0000000..54b154d --- /dev/null +++ b/polkaj-adapter-tests/build.gradle @@ -0,0 +1,20 @@ +//apply from: '../common_java_app.gradle' + +apply plugin: 'java-library' +apply plugin: 'groovy' + +dependencies { + api project(":polkaj-json-types") + api project(":polkaj-api-base") + + api 'org.mock-server:mockserver-netty:5.10' + api 'org.java-websocket:Java-WebSocket:1.5.1' + api 'org.objenesis:objenesis:3.1' + api ('org.spockframework:spock-core:2.0-M2-groovy-3.0') { + exclude group: 'org.codehaus.groovy' + } + api "nl.jqno.equalsverifier:equalsverifier:3.3" + api 'org.codehaus.groovy:groovy-all:3.0.3' + api 'commons-codec:commons-codec:1.14' + api 'cglib:cglib-nodep:3.3.0' +} \ No newline at end of file diff --git a/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/MockWsServer.groovy b/polkaj-adapter-tests/src/main/groovy/io/emeraldpay/polkaj/api/MockWsServer.groovy similarity index 93% rename from polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/MockWsServer.groovy rename to polkaj-adapter-tests/src/main/groovy/io/emeraldpay/polkaj/api/MockWsServer.groovy index 63db69f..053ab70 100644 --- a/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/MockWsServer.groovy +++ b/polkaj-adapter-tests/src/main/groovy/io/emeraldpay/polkaj/api/MockWsServer.groovy @@ -1,8 +1,6 @@ -package io.emeraldpay.polkaj.apiws +package io.emeraldpay.polkaj.api import org.java_websocket.WebSocket -import org.java_websocket.drafts.Draft_6455 -import org.java_websocket.extensions.DefaultExtension import org.java_websocket.handshake.ClientHandshake import org.java_websocket.server.WebSocketServer diff --git a/polkaj-api-http/src/test/groovy/io/emeraldpay/polkaj/apihttp/PolkadotHttpClientSpec.groovy b/polkaj-adapter-tests/src/main/groovy/io/emeraldpay/polkaj/api/RpcAdapterSpec.groovy similarity index 81% rename from polkaj-api-http/src/test/groovy/io/emeraldpay/polkaj/apihttp/PolkadotHttpClientSpec.groovy rename to polkaj-adapter-tests/src/main/groovy/io/emeraldpay/polkaj/api/RpcAdapterSpec.groovy index 686a7ed..779eef2 100644 --- a/polkaj-api-http/src/test/groovy/io/emeraldpay/polkaj/apihttp/PolkadotHttpClientSpec.groovy +++ b/polkaj-adapter-tests/src/main/groovy/io/emeraldpay/polkaj/api/RpcAdapterSpec.groovy @@ -1,9 +1,7 @@ -package io.emeraldpay.polkaj.apihttp +package io.emeraldpay.polkaj.api -import io.emeraldpay.polkaj.api.RpcCall -import io.emeraldpay.polkaj.types.Hash256 -import io.emeraldpay.polkaj.api.RpcException import io.emeraldpay.polkaj.json.BlockResponseJson +import io.emeraldpay.polkaj.types.Hash256 import org.mockserver.integration.ClientAndServer import org.mockserver.model.Delay import org.mockserver.model.HttpRequest @@ -17,25 +15,33 @@ import java.nio.charset.Charset import java.time.Duration import java.util.concurrent.ExecutionException -class PolkadotHttpClientSpec extends Specification { - PolkadotHttpApi client +abstract class RpcAdapterSpec extends Specification{ + + + PolkadotApi polkadotApi @Shared ClientAndServer mockServer def setup() { - client = PolkadotHttpApi.newBuilder() - .connectTo("http://localhost:18080") - .timeout(Duration.ofSeconds(1)) - .build() mockServer = ClientAndServer.startClientAndServer(18080) + def adapter = provideAdapter("http://localhost:18080", + "testuser", + "testpassword", + Duration.ofSeconds(1) + ) + polkadotApi = PolkadotApi.newBuilder() + .rpcCallAdapter(adapter) + .build() } def cleanup() { mockServer.stop() } + abstract RpcCallAdapter provideAdapter(String connectTo, String username, String password, Duration timeout) + def "Make request"() { setup: def response = '{\n' + @@ -51,7 +57,7 @@ class PolkadotHttpClientSpec extends Specification { HttpResponse.response(response).withContentType(MediaType.APPLICATION_JSON) ) when: - def act = client.execute(RpcCall.create(String, "chain_getFinalisedHead")) + def act = polkadotApi.execute(RpcCall.create(String, "chain_getFinalisedHead")) then: act.get() == "0x5d83f66b61701da4cbd7a60137db89c69469a4f798b62aba9176ab253b423828" @@ -70,13 +76,13 @@ class PolkadotHttpClientSpec extends Specification { HttpResponse.response(response).withContentType(MediaType.APPLICATION_JSON) ) when: - def act = client.execute(RpcCall.create(String, "chain_getFinalisedHead")) + def act = polkadotApi.execute(RpcCall.create(String, "chain_getFinalisedHead")) then: act.get() != null when: - client.close() - client.execute(RpcCall.create(String, "chain_getFinalisedHead")).get() + polkadotApi.close() + polkadotApi.execute(RpcCall.create(String, "chain_getFinalisedHead")).get() then: def t = thrown(ExecutionException) @@ -98,7 +104,7 @@ class PolkadotHttpClientSpec extends Specification { ) when: - client.execute(RpcCall.create(String, "chain_getFinalisedHead")).get() + polkadotApi.execute(RpcCall.create(String, "chain_getFinalisedHead")).get() then: def t = thrown(ExecutionException) @@ -133,7 +139,7 @@ class PolkadotHttpClientSpec extends Specification { HttpResponse.response(response).withContentType(MediaType.APPLICATION_JSON.withCharset(Charset.forName("UTF-8"))) ) when: - def act = client.execute(RpcCall.create(BlockResponseJson, "chain_getBlock", Hash256.from("0x9130103f8fbca52a79042211383946b39e6269b6ab49bc08035c9893d782c1bb"))).get() + def act = polkadotApi.execute(RpcCall.create(BlockResponseJson, "chain_getBlock", Hash256.from("0x9130103f8fbca52a79042211383946b39e6269b6ab49bc08035c9893d782c1bb"))).get() then: mockServer.verify( HttpRequest.request() @@ -157,7 +163,7 @@ class PolkadotHttpClientSpec extends Specification { HttpResponse.response("").withContentType(MediaType.APPLICATION_JSON).withStatusCode(503) ) when: - client.execute(RpcCall.create(String, "chain_getFinalisedHead")).get() + polkadotApi.execute(RpcCall.create(String, "chain_getFinalisedHead")).get() then: def t = thrown(ExecutionException) t.cause instanceof RpcException @@ -176,7 +182,7 @@ class PolkadotHttpClientSpec extends Specification { HttpResponse.response("").withContentType(MediaType.TEXT_PLAIN) ) when: - client.execute(RpcCall.create(String, "chain_getFinalisedHead")).get() + polkadotApi.execute(RpcCall.create(String, "chain_getFinalisedHead")).get() then: def t = thrown(ExecutionException) t.cause instanceof RpcException @@ -184,15 +190,11 @@ class PolkadotHttpClientSpec extends Specification { } - def "Send basic auth"() { + def "Process response error"() { setup: - client = PolkadotHttpApi.newBuilder() - .connectTo("http://localhost:18080") - .basicAuth("testuser", "testpassword") - .build() def response = '{' + ' "jsonrpc": "2.0",' + - ' "result": "0x5d83f66b61701da4cbd7a60137db89c69469a4f798b62aba9176ab253b423828",' + + ' "error": {"code": -32601, "message": "Method not found"},' + ' "id": 0' + '}' mockServer.when( @@ -201,23 +203,23 @@ class PolkadotHttpClientSpec extends Specification { HttpResponse.response(response).withContentType(MediaType.APPLICATION_JSON) ) when: - def act = client.execute(RpcCall.create(String, "chain_getFinalisedHead")).get() + polkadotApi.execute(RpcCall.create(String, "chain_getFinalisedHead")).get() then: - act == "0x5d83f66b61701da4cbd7a60137db89c69469a4f798b62aba9176ab253b423828" - mockServer.verify( - HttpRequest.request().withHeader("Authorization", "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk") - ) + def t = thrown(ExecutionException) + t.cause instanceof RpcException + with((RpcException)t.cause) { + code == -32601 + rpcMessage == "Method not found" + } } - def "Process response error"() { + def "Send basic auth"() { setup: - client = PolkadotHttpApi.newBuilder() - .connectTo("http://localhost:18080") - .build() + def response = '{' + ' "jsonrpc": "2.0",' + - ' "error": {"code": -32601, "message": "Method not found"},' + + ' "result": "0x5d83f66b61701da4cbd7a60137db89c69469a4f798b62aba9176ab253b423828",' + ' "id": 0' + '}' mockServer.when( @@ -226,15 +228,13 @@ class PolkadotHttpClientSpec extends Specification { HttpResponse.response(response).withContentType(MediaType.APPLICATION_JSON) ) when: - client.execute(RpcCall.create(String, "chain_getFinalisedHead")).get() + def act = polkadotApi.execute(RpcCall.create(String, "chain_getFinalisedHead")).get() then: - def t = thrown(ExecutionException) - t.cause instanceof RpcException - with((RpcException)t.cause) { - code == -32601 - rpcMessage == "Method not found" - } + act == "0x5d83f66b61701da4cbd7a60137db89c69469a4f798b62aba9176ab253b423828" + mockServer.verify( + HttpRequest.request().withHeader("Authorization", "Basic dGVzdHVzZXI6dGVzdHBhc3N3b3Jk") + ) } -} +} \ No newline at end of file diff --git a/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/PolkadotWsClientSpec.groovy b/polkaj-adapter-tests/src/main/groovy/io/emeraldpay/polkaj/api/SubscriptionAdapterSpec.groovy similarity index 51% rename from polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/PolkadotWsClientSpec.groovy rename to polkaj-adapter-tests/src/main/groovy/io/emeraldpay/polkaj/api/SubscriptionAdapterSpec.groovy index ecee1f3..704a8c1 100644 --- a/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/PolkadotWsClientSpec.groovy +++ b/polkaj-adapter-tests/src/main/groovy/io/emeraldpay/polkaj/api/SubscriptionAdapterSpec.groovy @@ -1,56 +1,51 @@ -package io.emeraldpay.polkaj.apiws +package io.emeraldpay.polkaj.api -import com.fasterxml.jackson.databind.ObjectMapper -import io.emeraldpay.polkaj.api.RpcCall -import io.emeraldpay.polkaj.api.RpcException -import io.emeraldpay.polkaj.api.SubscribeCall import io.emeraldpay.polkaj.json.BlockJson import io.emeraldpay.polkaj.types.Hash256 import spock.lang.Shared import spock.lang.Specification -import java.net.http.HttpClient import java.util.concurrent.ExecutionException import java.util.concurrent.TimeUnit -class PolkadotWsClientSpec extends Specification { +abstract class SubscriptionAdapterSpec extends Specification{ // needs large timeouts and sleep, especially on CI where it's much slower to run static TIMEOUT = 15 static SLEEP = 250 -// System.setProperty("jdk.httpclient.HttpClient.log", "all"); -// System.setProperty("jdk.internal.httpclient.websocket.debug", "true") - - // port may be held open a couple of seconds after stopping the test, need new port each time to avoid collision static int port = 19900 + new Random().nextInt(100) @Shared MockWsServer server + @Shared - PolkadotWsApi client + PolkadotApi polkadotApi def setup() { port++ server = new MockWsServer(port) server.start() Thread.sleep(SLEEP) - client = PolkadotWsApi.newBuilder() - .connectTo("ws://localhost:${port}") - .build() - assert client.connect().get(TIMEOUT, TimeUnit.SECONDS) + def adapter = provideAdapter("ws://localhost:${port}") + + polkadotApi = PolkadotApi.newBuilder() + .subscriptionAdapter(adapter).build() } def cleanup() { - client.close() + polkadotApi.close() server.stop() } + + abstract SubscriptionAdapter provideAdapter(String connectTo) + def "Subscribe to block"() { setup: List> received = [] when: server.onNextReply('{"jsonrpc":"2.0","result":"EsqruyKPnZvPZ6fr","id":0}') - def f = client.subscribe(SubscribeCall.create(BlockJson.Header.class, "chain_subscribeNewHead", "chain_unsubscribeNewHead")) + def f = polkadotApi.subscribe(SubscribeCall.create(BlockJson.Header.class, "chain_subscribeNewHead", "chain_unsubscribeNewHead")) def sub = f.get(TIMEOUT, TimeUnit.SECONDS) sub.handler({ event -> received.add([ @@ -75,60 +70,7 @@ class PolkadotWsClientSpec extends Specification { def "Make a request"() { when: server.onNextReply('{"jsonrpc":"2.0","result":"Hello World!","id":0}') - def f = client.execute(RpcCall.create(String.class, "test_foo")) - def act = f.get(TIMEOUT, TimeUnit.SECONDS) - then: - act == "Hello World!" - } - - def "Works with provided HttpClient"() { - setup: - client.close() - client = PolkadotWsApi.newBuilder() - .httpClient(HttpClient.newHttpClient()) - .connectTo("ws://localhost:${port}") - .build() - client.connect().get(TIMEOUT, TimeUnit.SECONDS) - when: - server.onNextReply('{"jsonrpc":"2.0","result":"Hello World!","id":0}') - def f = client.execute(RpcCall.create(String.class, "test_foo")) - def act = f.get(TIMEOUT, TimeUnit.SECONDS) - then: - act == "Hello World!" - } - - def "Works with provided ObjectMapper"() { - setup: - ObjectMapper objectMapper = Spy(new ObjectMapper()) - client.close() - client = PolkadotWsApi.newBuilder() - .objectMapper(objectMapper) - .connectTo("ws://localhost:${port}") - .build() - client.connect().get(TIMEOUT, TimeUnit.SECONDS) - when: - server.onNextReply('{"jsonrpc":"2.0","result":"Hello World!","id":0}') - def f = client.execute(RpcCall.create(String.class, "test_foo")) - def act = f.get(TIMEOUT, TimeUnit.SECONDS) - then: - act == "Hello World!" - (1.._) * objectMapper._(_, _) - } - - def "By default connects to 9944"() { - setup: - client.close() - server.stop() - println("Start new on 9944") - server = new MockWsServer(9944) - server.start() - Thread.sleep(SLEEP) - client = PolkadotWsApi.newBuilder() - .build() - client.connect().get(TIMEOUT, TimeUnit.SECONDS) - when: - server.onNextReply('{"jsonrpc":"2.0","result":"Hello World!","id":0}') - def f = client.execute(RpcCall.create(String.class, "test_foo")) + def f = polkadotApi.execute(RpcCall.create(String.class, "test_foo")) def act = f.get(TIMEOUT, TimeUnit.SECONDS) then: act == "Hello World!" @@ -137,7 +79,7 @@ class PolkadotWsClientSpec extends Specification { def "Fail to subscribe with invalid command"() { when: server.onNextReply('{"jsonrpc":"2.0","error":{"code": -1, "message": "Test", "data": "Test data"},"id":0}') - def f = client.subscribe(SubscribeCall.create(Hash256.class, "test_subscribeNone", "test_unsubscribeNone")) + def f = polkadotApi.subscribe(SubscribeCall.create(Hash256.class, "test_subscribeNone", "test_unsubscribeNone")) f.get(TIMEOUT, TimeUnit.SECONDS) then: def t = thrown(ExecutionException.class) @@ -151,7 +93,7 @@ class PolkadotWsClientSpec extends Specification { def "Ignores unknown responses"() { when: - def f = client.execute(RpcCall.create(String.class, "test_foo")) + def f = polkadotApi.execute(RpcCall.create(String.class, "test_foo")) Thread.sleep(SLEEP) server.reply('{"jsonrpc":"2.0","method":"test_none","params":{"result": "test", "subscription":101}}') server.reply('{"jsonrpc":"2.0","error":{"code": -1, "message": "Test"},"id":50}') @@ -161,5 +103,5 @@ class PolkadotWsClientSpec extends Specification { then: act == "right" } - } + diff --git a/polkaj-api-base/build.gradle b/polkaj-api-base/build.gradle index 808ba56..41ee3c8 100644 --- a/polkaj-api-base/build.gradle +++ b/polkaj-api-base/build.gradle @@ -1,3 +1,8 @@ + apply plugin: 'java' + apply plugin: 'java-library' + apply plugin: 'groovy' +apply from: '../common_java_app.gradle' + dependencies { api project(":polkaj-json-types") } \ No newline at end of file diff --git a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/PolkadotApi.java b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/PolkadotApi.java index 22efb50..8d9fd2a 100644 --- a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/PolkadotApi.java +++ b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/PolkadotApi.java @@ -5,7 +5,7 @@ /** * @see PolkadotMethod */ -public interface PolkadotApi { +public interface PolkadotApi extends AutoCloseable { /** * Execute JSON RPC request @@ -17,12 +17,58 @@ public interface PolkadotApi { */ CompletableFuture execute(RpcCall call); + /** + * Subscribe to a method that provides multiple responses + * + * @param call subscription call details to execute + * @param type of the result + * @return Subscription instance + */ + CompletableFuture> subscribe(SubscribeCall call); + /** * * @return Standard Polkadot RPC commands */ - public static StandardCommands commands() { + static StandardCommands commands() { return StandardCommands.getInstance(); } + static Builder newBuilder(){ + return new Builder(); + } + + class Builder { + + private Runnable onClose; + private RpcCallAdapter rpcCallAdapter; + private SubscriptionAdapter subscriptionAdapter; + + public Builder rpcCallAdapter(RpcCallAdapter adapter){ + this.rpcCallAdapter = adapter; + return this; + } + + public Builder subscriptionAdapter(SubscriptionAdapter adapter){ + this.rpcCallAdapter = adapter; + this.subscriptionAdapter = adapter; + return this; + } + + public Builder onClose(Runnable onClose) { + this.onClose = onClose; + return this; + } + + /** + * Apply configuration and build client + * + * @return new instance of PolkadotRpcClient + */ + public PolkadotApi build() { + return new PolkadotApiImpl(onClose, rpcCallAdapter, subscriptionAdapter); + } + } + + } diff --git a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/PolkadotApiImpl.java b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/PolkadotApiImpl.java new file mode 100644 index 0000000..c10dfad --- /dev/null +++ b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/PolkadotApiImpl.java @@ -0,0 +1,61 @@ +package io.emeraldpay.polkaj.api; + +import java.util.concurrent.CompletableFuture; + + +final class PolkadotApiImpl implements PolkadotApi { + + private boolean closed = false; + + private final Runnable onClose; + private final RpcCallAdapter rpcCallAdapter; + private final SubscriptionAdapter subcriptionAdapter; + + public PolkadotApiImpl(Runnable onClose, + RpcCallAdapter rpcCallAdapter, + SubscriptionAdapter subscriptionAdapter) { + this.onClose = onClose; + this.rpcCallAdapter = rpcCallAdapter; + this.subcriptionAdapter = subscriptionAdapter; + } + + @Override + public CompletableFuture execute(RpcCall call) { + if (closed) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new IllegalStateException("Client is already closed")); + return future; + } + if(rpcCallAdapter == null){ + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new IllegalStateException("RpcCallAdapter Not set")); + return future; + } + + return rpcCallAdapter.produceRpcFuture(call); + } + + @Override + public CompletableFuture> subscribe(SubscribeCall call) { + if (closed) { + CompletableFuture> future = new CompletableFuture<>(); + future.completeExceptionally(new IllegalStateException("Client is already closed")); + return future; + } + if(subcriptionAdapter == null){ + CompletableFuture> future = new CompletableFuture<>(); + future.completeExceptionally(new IllegalStateException("SubscriptionAdapter Not set")); + return future; + } + return subcriptionAdapter.subscribe(call); + } + + @Override + public void close() throws Exception { + if(closed) return; + closed = true; + if(rpcCallAdapter != null) rpcCallAdapter.close(); + if(subcriptionAdapter != null && subcriptionAdapter != rpcCallAdapter) subcriptionAdapter.close(); + if(onClose != null) onClose.run(); + } +} diff --git a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/RpcCallAdapter.java b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/RpcCallAdapter.java new file mode 100644 index 0000000..2d728ae --- /dev/null +++ b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/RpcCallAdapter.java @@ -0,0 +1,8 @@ +package io.emeraldpay.polkaj.api; + +import java.util.concurrent.CompletableFuture; + +public interface RpcCallAdapter extends AutoCloseable { + + CompletableFuture produceRpcFuture(RpcCall call); +} diff --git a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/AbstractPolkadotApi.java b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/RpcCoder.java similarity index 78% rename from polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/AbstractPolkadotApi.java rename to polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/RpcCoder.java index c1898f7..41bc2fd 100644 --- a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/AbstractPolkadotApi.java +++ b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/RpcCoder.java @@ -7,15 +7,27 @@ import java.util.concurrent.CompletionException; import java.util.concurrent.atomic.AtomicInteger; -public abstract class AbstractPolkadotApi implements PolkadotApi { +public class RpcCoder { - protected final AtomicInteger id = new AtomicInteger(0); - protected final ObjectMapper objectMapper; + private final ObjectMapper objectMapper; + private final AtomicInteger id = new AtomicInteger(0); - public AbstractPolkadotApi(ObjectMapper objectMapper) { + public RpcCoder(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } + public int nextId() { + return id.getAndIncrement(); + } + + public void resetId(){ + id.set(0); + } + + public ObjectMapper getObjectMapper() { + return objectMapper; + } + public JavaType responseType(JavaType resultType) { return objectMapper.getTypeFactory().constructType(resultType); } @@ -24,10 +36,6 @@ public JavaType responseType(Class resultClazz) { return objectMapper.getTypeFactory().constructType(resultClazz); } - public int nextId() { - return id.getAndIncrement(); - } - /** * Decode JSON RPC response * @@ -38,7 +46,7 @@ public int nextId() { * @return The decoded result * @throws CompletionException with RpcException details to let executor know that the response is invalid */ - public T decode(int id, String content, JavaType clazz) { + final public T decode(int id, String content, JavaType clazz) { JavaType type = objectMapper.getTypeFactory().constructParametricType(RpcResponse.class, clazz); RpcResponse response; try { @@ -65,13 +73,12 @@ public T decode(int id, String content, JavaType clazz) { * Encode RPC request as JSON * * @param id id of the request - * @param method method name - * @param params params + * @param call the RpcCall to encode * @return full JSON of the request * @throws JsonProcessingException if cannot encode some of the params into JSON */ - public byte[] encode(int id, String method, Object[] params) throws JsonProcessingException { - RpcRequest request = new RpcRequest(id, method, params); + final public byte[] encode(int id, RpcCall call) throws JsonProcessingException { + RpcRequest request = new RpcRequest(id, call.getMethod(), call.getParams()); return objectMapper.writeValueAsBytes(request); } } diff --git a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/PolkadotSubscriptionApi.java b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/SubscriptionAdapter.java similarity index 90% rename from polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/PolkadotSubscriptionApi.java rename to polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/SubscriptionAdapter.java index fa6b566..ce7a7bf 100644 --- a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/PolkadotSubscriptionApi.java +++ b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/SubscriptionAdapter.java @@ -5,7 +5,8 @@ /** * @see PolkadotMethod */ -public interface PolkadotSubscriptionApi { +public interface SubscriptionAdapter extends RpcCallAdapter{ + /** * Subscribe to a method that provides multiple responses @@ -23,4 +24,6 @@ public interface PolkadotSubscriptionApi { public static StandardSubscriptions subscriptions() { return StandardSubscriptions.getInstance(); } + + } diff --git a/polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api/PolkadotApiSpec.groovy b/polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api/PolkadotApiSpec.groovy new file mode 100644 index 0000000..fa7bd7f --- /dev/null +++ b/polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api/PolkadotApiSpec.groovy @@ -0,0 +1,157 @@ +package io.emeraldpay.polkaj.api + +import spock.lang.Specification + +import java.util.concurrent.ExecutionException + +class PolkadotApiSpec extends Specification{ + + RpcCallAdapter rpcCallAdapter = Mock() + SubscriptionAdapter subscriptionAdapter = Mock() + Runnable onClose = Mock() + + def "close is passed to all listeners"(){ + setup: + def polkadotApi = PolkadotApi.newBuilder() + .subscriptionAdapter(subscriptionAdapter) + .rpcCallAdapter(rpcCallAdapter) + .onClose(onClose) + .build() + when: + polkadotApi.close() + then: + 1 * rpcCallAdapter.close() + 1 * subscriptionAdapter.close() + 1 * onClose.run() + } + + def "does not call close twice on subscriptionAdapter"(){ + setup: + def polkadotApi = PolkadotApi.newBuilder() + .subscriptionAdapter(subscriptionAdapter) + .onClose(onClose) + .build() + when: + polkadotApi.close() + then: + 1 * subscriptionAdapter.close() + 1 * onClose.run() + } + + def "uses RpcCallAdapter when call is executed"(){ + setup: + def polkadotApi = PolkadotApi.newBuilder() + .rpcCallAdapter(rpcCallAdapter) + .onClose(onClose) + .build() + def call = RpcCall.create(String, "test") + when: + polkadotApi.execute(call) + then: + 1 * rpcCallAdapter.produceRpcFuture(call) + } + + def "uses SubscriptionAdapter for RPC calls"(){ + setup: + def polkadotApi = PolkadotApi.newBuilder() + .subscriptionAdapter(subscriptionAdapter) + .onClose(onClose) + .build() + def call = RpcCall.create(String, "test") + when: + polkadotApi.execute(call) + then: + 1 * subscriptionAdapter.produceRpcFuture(call) + } + + def "uses SubscriptionAdapter when subscribed"(){ + setup: + def polkadotApi = PolkadotApi.newBuilder() + .subscriptionAdapter(subscriptionAdapter) + .onClose(onClose) + .build() + def call = SubscribeCall.create(String, "test", "test") + when: + polkadotApi.subscribe(call) + then: + 1 * subscriptionAdapter.subscribe(call) + } + + def "execution when closed causes exception"(){ + setup: + def polkadotApi = PolkadotApi.newBuilder() + .rpcCallAdapter(rpcCallAdapter) + .onClose(onClose) + .build() + def call = RpcCall.create(String, "test") + polkadotApi.close() + when: + polkadotApi.execute(call).get() + then: + def t = thrown(ExecutionException) + t.cause instanceof IllegalStateException + 0 * rpcCallAdapter.produceRpcFuture(call) + } + + def "execution when closed causes exeption subscribe adapter"(){ + setup: + def polkadotApi = PolkadotApi.newBuilder() + .subscriptionAdapter(subscriptionAdapter) + .onClose(onClose) + .build() + def call = RpcCall.create(String, "test") + polkadotApi.close() + when: + polkadotApi.execute(call).get() + then: + def t = thrown(ExecutionException) + t.cause instanceof IllegalStateException + 0 * subscriptionAdapter.produceRpcFuture(call) + } + + def "subscription when closed causes exeption"(){ + setup: + def polkadotApi = PolkadotApi.newBuilder() + .subscriptionAdapter(subscriptionAdapter) + .onClose(onClose) + .build() + def call = SubscribeCall.create(String, "test", "test") + polkadotApi.close() + when: + polkadotApi.subscribe(call).get() + then: + def t = thrown(ExecutionException) + t.cause instanceof IllegalStateException + 0 * subscriptionAdapter.subscribe(call) + } + + def "throws exception when no adapter set on subscribe"(){ + setup: + def polkadotApi = PolkadotApi.newBuilder() + .onClose(onClose) + .build() + def call = SubscribeCall.create(String, "test", "test") + when: + polkadotApi.subscribe(call).get() + then: + def t = thrown(ExecutionException) + t.cause instanceof IllegalStateException + } + + def "throws exception no no adapter set on execute"(){ + setup: + def polkadotApi = PolkadotApi.newBuilder() + .onClose(onClose) + .build() + def call = RpcCall.create(String, "test") + when: + polkadotApi.execute(call).get() + then: + def t = thrown(ExecutionException) + t.cause instanceof IllegalStateException + } + + def "returns StandardCommands shortcut"(){ + assert PolkadotApi.commands() == StandardCommands.getInstance(); + } +} diff --git a/polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api/AbstractPolkadotApiSpec.groovy b/polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api/RpcCoderSpec.groovy similarity index 74% rename from polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api/AbstractPolkadotApiSpec.groovy rename to polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api/RpcCoderSpec.groovy index de6c7b4..c1d583e 100644 --- a/polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api/AbstractPolkadotApiSpec.groovy +++ b/polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api/RpcCoderSpec.groovy @@ -6,37 +6,36 @@ import io.emeraldpay.polkaj.json.BlockResponseJson import io.emeraldpay.polkaj.json.jackson.PolkadotModule import spock.lang.Specification -import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletionException -class AbstractPolkadotApiSpec extends Specification { +class RpcCoderSpec extends Specification { - AbstractPolkadotApi client = new TestingPolkadotApi(new ObjectMapper().tap { registerModule(new PolkadotModule())}) + RpcCoder rpcCoder = new RpcCoder(new ObjectMapper().tap { registerModule(new PolkadotModule())}) def "Encode empty params request"() { when: - def act = client.encode(1, "test_foo") + def act = rpcCoder.encode(1, RpcCall.create(Void.class, "test_foo")) then: new String(act) == '{"jsonrpc":"2.0","id":1,"method":"test_foo","params":[]}' } def "Encode single param request"() { when: - def act = client.encode(1, "test_foo", "hello") + def act = rpcCoder.encode(1, RpcCall.create(Void.class, "test_foo", "hello")) then: new String(act) == '{"jsonrpc":"2.0","id":1,"method":"test_foo","params":["hello"]}' } def "Encode multi params request"() { when: - def act = client.encode(2, "test_foo", Hash256.from("0x5d83f66b61701da4cbd7a60137db89c69469a4f798b62aba9176ab253b423828"), 100, false) + def act = rpcCoder.encode(2, RpcCall.create(Void.class,"test_foo", Hash256.from("0x5d83f66b61701da4cbd7a60137db89c69469a4f798b62aba9176ab253b423828"), 100, false)) then: new String(act) == '{"jsonrpc":"2.0","id":2,"method":"test_foo","params":["0x5d83f66b61701da4cbd7a60137db89c69469a4f798b62aba9176ab253b423828",100,false]}' } def "Encode object params request"() { when: - def act = client.encode(3, "test_foo", [foo: "bar", baz: 1]) + def act = rpcCoder.encode(3, RpcCall.create(Void.class,"test_foo", [foo: "bar", baz: 1])) then: new String(act) == '{"jsonrpc":"2.0","id":3,"method":"test_foo","params":[{"foo":"bar","baz":1}]}' } @@ -49,7 +48,7 @@ class AbstractPolkadotApiSpec extends Specification { ' "id": 0\n' + '}' when: - def act = client.decode(0, response, client.responseType(String)) + def act = rpcCoder.decode(0, response, rpcCoder.responseType(String)) then: act == '0x5d83f66b61701da4cbd7a60137db89c69469a4f798b62aba9176ab253b423828' } @@ -62,7 +61,7 @@ class AbstractPolkadotApiSpec extends Specification { ' "id": 0\n' + '}' when: - def act = client.decode(0, response, client.responseType(BlockResponseJson)) + def act = rpcCoder.decode(0, response, rpcCoder.responseType(BlockResponseJson)) then: act == null } @@ -75,7 +74,7 @@ class AbstractPolkadotApiSpec extends Specification { ' "id": 0\n' + '}' when: - def act = client.decode(0, response, client.responseType(Hash256)) + def act = rpcCoder.decode(0, response, rpcCoder.responseType(Hash256)) then: act == Hash256.from('0x5d83f66b61701da4cbd7a60137db89c69469a4f798b62aba9176ab253b423828') } @@ -88,7 +87,7 @@ class AbstractPolkadotApiSpec extends Specification { ' "id": 0\n' + '}' when: - client.decode(0, response, client.responseType(BlockResponseJson)) + rpcCoder.decode(0, response, rpcCoder.responseType(BlockResponseJson)) then: def t = thrown(CompletionException) t.cause instanceof RpcException @@ -108,7 +107,7 @@ class AbstractPolkadotApiSpec extends Specification { ' "id": 0\n' + '}' when: - client.decode(1, response, client.responseType(String)) + rpcCoder.decode(1, response, rpcCoder.responseType(String)) then: def t = thrown(CompletionException) t.cause instanceof RpcException @@ -124,7 +123,7 @@ class AbstractPolkadotApiSpec extends Specification { setup: def response = '{\n "jsonrpc": "2' when: - client.decode(1, response, client.responseType(String)) + rpcCoder.decode(1, response, rpcCoder.responseType(String)) then: def t = thrown(CompletionException) t.cause instanceof RpcException @@ -136,14 +135,4 @@ class AbstractPolkadotApiSpec extends Specification { } } - class TestingPolkadotApi extends AbstractPolkadotApi { - TestingPolkadotApi(ObjectMapper objectMapper) { - super(objectMapper) - } - - @Override - def CompletableFuture execute(RpcCall call) { - throw new UnsupportedOperationException("Execute is not implemented for the test") - } - } } diff --git a/polkaj-api-http/build.gradle b/polkaj-api-http/build.gradle index 2ed8a5a..64c53ec 100644 --- a/polkaj-api-http/build.gradle +++ b/polkaj-api-http/build.gradle @@ -1,6 +1,14 @@ +apply from: '../common_java_app.gradle' + +compileJava { + targetCompatibility = '11' + sourceCompatibility = '11' +} + dependencies { api project(":polkaj-json-types") api project(":polkaj-api-base") testImplementation 'org.mock-server:mockserver-netty:5.10' + testImplementation project(":polkaj-adapter-tests") } \ No newline at end of file diff --git a/polkaj-api-http/src/main/java/io/emeraldpay/polkaj/apihttp/PolkadotHttpApi.java b/polkaj-api-http/src/main/java/io/emeraldpay/polkaj/apihttp/JavaHttpAdapter.java similarity index 81% rename from polkaj-api-http/src/main/java/io/emeraldpay/polkaj/apihttp/PolkadotHttpApi.java rename to polkaj-api-http/src/main/java/io/emeraldpay/polkaj/apihttp/JavaHttpAdapter.java index 22aed3c..238e15a 100644 --- a/polkaj-api-http/src/main/java/io/emeraldpay/polkaj/apihttp/PolkadotHttpApi.java +++ b/polkaj-api-http/src/main/java/io/emeraldpay/polkaj/apihttp/JavaHttpAdapter.java @@ -3,9 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; -import io.emeraldpay.polkaj.api.AbstractPolkadotApi; -import io.emeraldpay.polkaj.api.RpcCall; -import io.emeraldpay.polkaj.api.RpcException; +import io.emeraldpay.polkaj.api.*; import io.emeraldpay.polkaj.json.jackson.PolkadotModule; import java.net.URI; @@ -23,9 +21,9 @@ /** * Default JSON RPC HTTP client for Polkadot API. It uses Java 11 HttpClient implementation for requests. * Each request made from that client has a uniq id, from a monotone sequence starting on 0. A new instance is - * supposed to be create through {@link PolkadotHttpApi#newBuilder()}: + * supposed to be create through {@link JavaHttpAdapter#newBuilder()}: *
- * The class is AutoCloseable, with {@link PolkadotHttpApi#close()} methods, which shutdown a thread (or threads) used for http requests. + * The class is AutoCloseable, with {@link JavaHttpAdapter#close()} methods, which shutdown a thread (or threads) used for http requests. * *
* Example: @@ -35,19 +33,19 @@ * System.out.println("Current head: " + hash.get()); * */ -public class PolkadotHttpApi extends AbstractPolkadotApi implements AutoCloseable { +public class JavaHttpAdapter implements RpcCallAdapter { private static final String APPLICATION_JSON = "application/json"; private final HttpClient httpClient; - private final Runnable onClose; private final HttpRequest.Builder request; + private final Runnable onClose; + private final RpcCoder rpcCoder; + private boolean closed = false; - private PolkadotHttpApi(URI target, HttpClient httpClient, String basicAuth, Runnable onClose, ObjectMapper objectMapper, Duration timeout) { - super(objectMapper); + private JavaHttpAdapter(URI target, HttpClient httpClient, String basicAuth, Duration timeout, Runnable onClose, RpcCoder rpcCoder) { this.httpClient = httpClient; - this.onClose = onClose; HttpRequest.Builder request = HttpRequest.newBuilder() .uri(target) @@ -60,6 +58,8 @@ private PolkadotHttpApi(URI target, HttpClient httpClient, String basicAuth, Run } this.request = request; + this.onClose = onClose; + this.rpcCoder = rpcCoder; } /** @@ -71,25 +71,28 @@ private PolkadotHttpApi(URI target, HttpClient httpClient, String basicAuth, Run * @see RpcException */ @Override - public CompletableFuture execute(RpcCall call) { + public CompletableFuture produceRpcFuture(RpcCall call) { if (closed) { return CompletableFuture.failedFuture( new IllegalStateException("Client is already closed") ); } - int id = nextId(); + final ObjectMapper objectMapper = rpcCoder.getObjectMapper(); + int id = rpcCoder.nextId(); JavaType type = call.getResultType(objectMapper.getTypeFactory()); try { HttpRequest.Builder request = this.request.copy() - .POST(HttpRequest.BodyPublishers.ofByteArray(encode(id, call.getMethod(), call.getParams()))); + .POST(HttpRequest.BodyPublishers.ofByteArray(rpcCoder.encode(id, call))); return httpClient.sendAsync(request.build(), HttpResponse.BodyHandlers.ofString()) .thenApply(this::verify) .thenApply(HttpResponse::body) - .thenApply(content -> decode(id, content, type)); + .thenApply(content -> rpcCoder.decode(id, content, type)); } catch (JsonProcessingException e) { return CompletableFuture.failedFuture( new RpcException(-32600, "Unable to encode request as JSON: " + e.getMessage(), e) ); + } catch (CompletionException e){ + return CompletableFuture.failedFuture(e); } } @@ -122,14 +125,18 @@ public static Builder newBuilder() { } @Override - public void close() throws Exception { + public void close() { if (closed) { return; } + closed = true; if (onClose != null) { - onClose.run(); + try { + onClose.run(); + } catch (Throwable t) { + System.err.println("Error during onClose call: " + t.getMessage()); + } } - closed = true; } /** @@ -137,7 +144,7 @@ public void close() throws Exception { * a standard Java HttpClient without any authorization connecting to localhost:9933 and using * a new instance of a Jackson ObjectMapper with PolkadotModule enabled. * - * @see PolkadotHttpApi + * @see JavaHttpAdapter * @see HttpClient * @see PolkadotModule */ @@ -146,11 +153,10 @@ public static class Builder { private ExecutorService executorService; private String basicAuth; private HttpClient httpClient; + private RpcCoder rpcCoder; private Runnable onClose; - private ObjectMapper objectMapper; private Duration timeout; - /** * Setup Basic Auth for RPC calls * @@ -211,21 +217,17 @@ public Builder executor(ExecutorService executorService) { throw new IllegalStateException("Custom HttpClient cannot be used with separate Executor"); } this.executorService = executorService; - if (this.onClose != null) { - this.onClose.run(); - } - this.onClose = null; return this; } /** - * Provide a custom ObjectMapper that will be used to encode/decode request and responses. + * Provide custom cleanup method. * - * @param objectMapper ObjectMapper + * @param onClose to be called on close. * @return builder */ - public Builder objectMapper(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; + public Builder onClose(Runnable onClose){ + this.onClose = onClose; return this; } @@ -240,33 +242,39 @@ public Builder timeout(Duration timeout) { return this; } - protected void initDefaults() { + /** + * Provide a custom RpcCoder for rpc serialization. + * + * @param rpcCoder rpcCoder + * @return builder + */ + public JavaHttpAdapter.Builder rpcCoder(RpcCoder rpcCoder){ + this.rpcCoder = rpcCoder; + return this; + } + + private void initDefaults() { + if (rpcCoder == null) { + final ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new PolkadotModule()); + rpcCoder = new RpcCoder(objectMapper); + } + if (httpClient == null && target == null) { try { connectTo("http://127.0.0.1:9933"); } catch (URISyntaxException e) { } } + if (executorService == null) { ExecutorService executorService = Executors.newCachedThreadPool(); this.executorService = executorService; onClose = executorService::shutdown; } - if (objectMapper == null) { - objectMapper = new ObjectMapper(); - objectMapper.registerModule(new PolkadotModule()); - } + if (timeout == null) { timeout = Duration.ofMinutes(1); } - } - - /** - * Apply configuration and build client - * - * @return new instance of PolkadotRpcClient - */ - public PolkadotHttpApi build() { - initDefaults(); if (this.httpClient == null) { httpClient = HttpClient.newBuilder() @@ -275,8 +283,16 @@ public PolkadotHttpApi build() { .followRedirects(HttpClient.Redirect.NEVER) .build(); } + } - return new PolkadotHttpApi(target, httpClient, basicAuth, onClose, objectMapper, timeout); + /** + * Apply configuration and build client + * + * @return new instance of PolkadotRpcClient + */ + public JavaHttpAdapter build() { + initDefaults(); + return new JavaHttpAdapter(target, httpClient, basicAuth, timeout, onClose, rpcCoder); } } diff --git a/polkaj-api-http/src/test/groovy/io/emeraldpay/polkaj/apihttp/JavaHttpAdapterSpec.groovy b/polkaj-api-http/src/test/groovy/io/emeraldpay/polkaj/apihttp/JavaHttpAdapterSpec.groovy new file mode 100644 index 0000000..8e8aebc --- /dev/null +++ b/polkaj-api-http/src/test/groovy/io/emeraldpay/polkaj/apihttp/JavaHttpAdapterSpec.groovy @@ -0,0 +1,53 @@ +package io.emeraldpay.polkaj.apihttp + +import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.polkaj.api.RpcCall +import io.emeraldpay.polkaj.api.RpcCallAdapter +import io.emeraldpay.polkaj.api.RpcCoder +import io.emeraldpay.polkaj.api.RpcAdapterSpec +import io.emeraldpay.polkaj.json.jackson.PolkadotModule + +import java.time.Duration +import java.util.concurrent.ExecutorService + +class JavaHttpAdapterSpec extends RpcAdapterSpec { + + @Override + RpcCallAdapter provideAdapter(String connectTo, String username, String password, Duration timeout) { + return JavaHttpAdapter.newBuilder() + .connectTo("http://localhost:18080") + .basicAuth(username, password) + .timeout(timeout) + .build() + } + + def "Uses provided onClose"(){ + setup: + def onClose = Spy(Runnable.class) + def executor = Spy(ExecutorService.class) + def adapter = JavaHttpAdapter.newBuilder() + .executor(executor) + .onClose(onClose) + .build() + when: + adapter.close() + then: + 1 * onClose.run() + } + + def "Uses provided RpcCoder"(){ + setup: + def objectMapper = new ObjectMapper().tap { + registerModule(new PolkadotModule()) + } + def rpcCoder = Spy(new RpcCoder(objectMapper)) + def adapter = JavaHttpAdapter.newBuilder() + .rpcCoder(rpcCoder) + .build() + when: + adapter.produceRpcFuture(RpcCall.create(String.class, "test")) + then: + 1 * rpcCoder.nextId() + } + +} diff --git a/polkaj-api-ws/build.gradle b/polkaj-api-ws/build.gradle index af6f0a5..496ee64 100644 --- a/polkaj-api-ws/build.gradle +++ b/polkaj-api-ws/build.gradle @@ -1,6 +1,15 @@ +apply from: '../common_java_app.gradle' + +compileJava { + targetCompatibility = '11' + sourceCompatibility = '11' +} + + dependencies { api project(":polkaj-json-types") api project(":polkaj-api-base") testImplementation 'org.java-websocket:Java-WebSocket:1.5.1' + testImplementation project(":polkaj-adapter-tests") } \ No newline at end of file diff --git a/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/DecodeResponse.java b/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/DecodeResponse.java index f79e1be..79ee79d 100644 --- a/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/DecodeResponse.java +++ b/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/DecodeResponse.java @@ -19,17 +19,17 @@ */ public class DecodeResponse { - private final ObjectMapper objectMapper; private final TypeMapping subscriptionMapping; private final TypeMapping rpcMapping; + private final ObjectMapper objectMapper; public DecodeResponse(ObjectMapper objectMapper, TypeMapping rpcMapping, TypeMapping subscriptionMapping) { - this.objectMapper = objectMapper; this.rpcMapping = rpcMapping; this.subscriptionMapping = subscriptionMapping; + this.objectMapper = objectMapper; } - public WsResponse decode(String json) throws IOException { + public WsResponse decode(final String json) throws IOException { JsonFactory jsonFactory = objectMapper.getFactory(); JsonParser parser = jsonFactory.createParser(json); if (parser.nextToken() != JsonToken.START_OBJECT) { @@ -74,14 +74,14 @@ public WsResponse decode(String json) throws IOException { method = parser.getValueAsString(); if (value != null) { return WsResponse.subscription( - new PolkadotWsApi.SubscriptionResponse<>(value.getId(), method, value.getValue()) + new JavaHttpSubscriptionAdapter.SubscriptionResponse<>(value.getId(), method, value.getValue()) ); } } else if ("params".equals(field)) { value = decodeSubscription(subscriptionMapping, parser); if (method != null) { return WsResponse.subscription( - new PolkadotWsApi.SubscriptionResponse<>(value.getId(), method, value.getValue()) + new JavaHttpSubscriptionAdapter.SubscriptionResponse<>(value.getId(), method, value.getValue()) ); } } @@ -192,7 +192,7 @@ public WsResponse.IdValue build() throws IOException { throw new IllegalStateException("Id is not set"); } if (error != null) { - return new WsResponse.IdValue(id, error); + return new WsResponse.IdValue<>(id, error); } if (type == null) { throw new IllegalStateException("Type is not set"); @@ -201,7 +201,7 @@ public WsResponse.IdValue build() throws IOException { Object value = objectMapper .readerFor(type) .readValue(node.traverse(objectMapper)); - return new WsResponse.IdValue(id, value); + return new WsResponse.IdValue<>(id, value); } throw new IllegalStateException("Not ready"); } diff --git a/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/DefaultSubscription.java b/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/DefaultSubscription.java index 4348f3a..13e728e 100644 --- a/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/DefaultSubscription.java +++ b/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/DefaultSubscription.java @@ -11,13 +11,13 @@ public class DefaultSubscription implements Subscription, Consumer> handlers; - public DefaultSubscription(JavaType type, String unsubscribeMethod, PolkadotWsApi client) { + public DefaultSubscription(JavaType type, String unsubscribeMethod, JavaHttpSubscriptionAdapter client) { this.type = type; this.unsubscribeMethod = unsubscribeMethod; - this.client = client; + this.adapter = client; } public String getId() { @@ -50,12 +50,12 @@ public void accept(Subscription.Event event) { } @Override - public void close() throws Exception { + public void close(){ if (id == null) { return; } - client.execute(RpcCall.create(Boolean.class, unsubscribeMethod, id)); - client.removeSubscription(id); + adapter.produceRpcFuture(RpcCall.create(Boolean.class, unsubscribeMethod, id)); + adapter.removeSubscription(id); } } diff --git a/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/PolkadotWsApi.java b/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/JavaHttpSubscriptionAdapter.java similarity index 80% rename from polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/PolkadotWsApi.java rename to polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/JavaHttpSubscriptionAdapter.java index 954360e..7b15b52 100644 --- a/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/PolkadotWsApi.java +++ b/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/JavaHttpSubscriptionAdapter.java @@ -14,22 +14,21 @@ import java.time.Duration; import java.util.Objects; import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; /** * WebSocket based client to Polkadot API. In addition to standard RPC calls it supports subscription to events, i.e. * when a call provides multiple responses. *
- * Before making calls, a {@link PolkadotWsApi#connect()} must be called to establish a connection. + * Before making calls, a {@link JavaHttpSubscriptionAdapter#connect()} must be called to establish a connection. */ -public class PolkadotWsApi extends AbstractPolkadotApi implements AutoCloseable, PolkadotSubscriptionApi { +public class JavaHttpSubscriptionAdapter implements SubscriptionAdapter, RpcCallAdapter { - private final AtomicInteger id = new AtomicInteger(0); private final AtomicReference webSocket = new AtomicReference<>(null); private final ConcurrentHashMap> execution = new ConcurrentHashMap<>(); private final ConcurrentHashMap> subscriptions = new ConcurrentHashMap<>(); private final URI target; + private final RpcCoder rpcCoder; private final DecodeResponse decodeResponse; private final HttpClient httpClient; private final Runnable onClose; @@ -37,8 +36,7 @@ public class PolkadotWsApi extends AbstractPolkadotApi implements AutoCloseable, private final ScheduledExecutorService control = Executors.newSingleThreadScheduledExecutor(); private final MessageBuffer messageBuffer = new MessageBuffer(); - private PolkadotWsApi(URI target, HttpClient httpClient, ObjectMapper objectMapper, Runnable onClose) { - super(objectMapper); + private JavaHttpSubscriptionAdapter(URI target, HttpClient httpClient, Runnable onClose, RpcCoder rpcCoder) { this.target = target; this.httpClient = httpClient; this.onClose = onClose; @@ -62,7 +60,8 @@ public JavaType get(String id) { return x.getType(); } }; - this.decodeResponse = new DecodeResponse(objectMapper, rpcMapping, subMapping); + this.rpcCoder = rpcCoder; + this.decodeResponse = new DecodeResponse(rpcCoder.getObjectMapper(), rpcMapping, subMapping); } public static Builder newBuilder() { @@ -91,7 +90,7 @@ public CompletableFuture connect() { } execution.clear(); subscriptions.clear(); - id.set(0); + rpcCoder.resetId(); // need to send ping, otherwise remote can drop the connection control.scheduleAtFixedRate(() -> { @@ -103,7 +102,7 @@ public CompletableFuture connect() { })).thenCombine(whenConnected, (webSocket, isOpen) -> isOpen); } - protected WebSocket.Listener newListener(final CompletableFuture whenConnected) { + private WebSocket.Listener newListener(final CompletableFuture whenConnected) { return new WebSocket.Listener() { @Override public CompletionStage onText(WebSocket webSocket, CharSequence data, boolean last) { @@ -139,29 +138,33 @@ public void onOpen(WebSocket webSocket) { public CompletionStage onPing(WebSocket webSocket, ByteBuffer message) { return webSocket.sendPong(message); } + }; } @Override - public CompletableFuture execute(RpcCall call) { - int id = this.id.getAndIncrement(); + public CompletableFuture produceRpcFuture(final RpcCall call) { + int id = rpcCoder.nextId(); byte[] payload; + final ObjectMapper objectMapper = rpcCoder.getObjectMapper(); try { - payload = encode(id, call.getMethod(), call.getParams()); + payload = rpcCoder.encode(id, call); } catch (JsonProcessingException e) { return CompletableFuture.failedFuture(e); } CompletableFuture whenResponseReceived = new CompletableFuture<>(); - execution.put(id, new RequestExpectation(responseType(call.getResultType(objectMapper.getTypeFactory())), whenResponseReceived)); + execution.put(id, new RequestExpectation<>(rpcCoder.responseType( + call.getResultType(objectMapper.getTypeFactory())), whenResponseReceived)); return webSocket.get() .sendText(new String(payload), true) .thenCombine(whenResponseReceived, (a, b) -> b); } + @Override - public CompletableFuture> subscribe(SubscribeCall call) { - var subscription = new DefaultSubscription(call.getResultType(objectMapper.getTypeFactory()), call.getUnsubscribe(), this); - var start = this.execute(RpcCall.create(String.class, call.getMethod(), call.getParams())); + public CompletableFuture> subscribe(final SubscribeCall call) { + var subscription = new DefaultSubscription(call.getResultType(rpcCoder.getObjectMapper().getTypeFactory()), call.getUnsubscribe(),this); + var start = this.produceRpcFuture(RpcCall.create(String.class, call.getMethod(), call.getParams())); return start.thenApply(id -> { subscriptions.put(id, subscription); subscription.setId(id); @@ -194,7 +197,7 @@ public void accept(SubscriptionResponse response) { if (s == null) { return; } - s.accept(new Subscription.Event(response.method, response.value)); + s.accept(new Subscription.Event<>(response.method, response.value)); } public boolean removeSubscription(String id) { @@ -202,7 +205,7 @@ public boolean removeSubscription(String id) { } @Override - public void close() throws Exception { + public void close() { webSocket.updateAndGet(old -> { if (old != null) { old.sendClose(WebSocket.NORMAL_CLOSURE, "close"); @@ -282,8 +285,8 @@ public static class Builder { private URI target; private ExecutorService executorService; private HttpClient httpClient; + private RpcCoder rpcCoder; private Runnable onClose; - private ObjectMapper objectMapper; /** * Server address URL @@ -292,7 +295,7 @@ public static class Builder { * @return builder * @throws URISyntaxException if specified url is invalid */ - public PolkadotWsApi.Builder connectTo(String target) throws URISyntaxException { + public JavaHttpSubscriptionAdapter.Builder connectTo(String target) throws URISyntaxException { return this.connectTo(new URI(target)); } @@ -302,7 +305,7 @@ public PolkadotWsApi.Builder connectTo(String target) throws URISyntaxException * @param target URL * @return builder */ - public PolkadotWsApi.Builder connectTo(URI target) { + public JavaHttpSubscriptionAdapter.Builder connectTo(URI target) { this.httpClient = null; this.target = target; return this; @@ -314,7 +317,7 @@ public PolkadotWsApi.Builder connectTo(URI target) { * @param httpClient client * @return builder */ - public PolkadotWsApi.Builder httpClient(HttpClient httpClient) { + public JavaHttpSubscriptionAdapter.Builder httpClient(HttpClient httpClient) { if (this.executorService != null) { throw new IllegalStateException("Custom HttpClient cannot be used with separate Executor"); } @@ -328,53 +331,55 @@ public PolkadotWsApi.Builder httpClient(HttpClient httpClient) { * @param executorService executor * @return builder */ - public PolkadotWsApi.Builder executor(ExecutorService executorService) { + public JavaHttpSubscriptionAdapter.Builder executor(ExecutorService executorService) { if (this.httpClient != null) { throw new IllegalStateException("Custom HttpClient cannot be used with separate Executor"); } this.executorService = executorService; - if (this.onClose != null) { - this.onClose.run(); - } - this.onClose = null; return this; } /** - * Provide a custom ObjectMapper that will be used to encode/decode request and responses. + * Provide custom cleanup method. * - * @param objectMapper ObjectMapper + * @param onClose to be called on close. * @return builder */ - public PolkadotWsApi.Builder objectMapper(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; + public Builder onClose(Runnable onClose){ + this.onClose = onClose; return this; } - protected void initDefaults() { + /** + * Provide a custom RpcCoder for rpc serialization. + * + * @param rpcCoder rpcCoder + * @return builder + */ + public JavaHttpSubscriptionAdapter.Builder rpcCoder(RpcCoder rpcCoder){ + this.rpcCoder = rpcCoder; + return this; + } + + + private void initDefaults() { + if (rpcCoder == null) { + final ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new PolkadotModule()); + rpcCoder = new RpcCoder(objectMapper); + } if (httpClient == null && target == null) { try { connectTo("ws://127.0.0.1:9944"); - } catch (URISyntaxException e) { } + } catch (URISyntaxException e) { + //wont happen + } } if (executorService == null) { ExecutorService executorService = Executors.newCachedThreadPool(); this.executorService = executorService; onClose = executorService::shutdownNow; } - if (objectMapper == null) { - objectMapper = new ObjectMapper(); - objectMapper.registerModule(new PolkadotModule()); - } - } - - /** - * Apply configuration and build client - * - * @return new instance of PolkadotRpcClient - */ - public PolkadotWsApi build() { - initDefaults(); if (this.httpClient == null) { httpClient = HttpClient.newBuilder() @@ -383,11 +388,17 @@ public PolkadotWsApi build() { .followRedirects(HttpClient.Redirect.NEVER) .build(); } - - return new PolkadotWsApi(target, httpClient, objectMapper, onClose); } - + /** + * Apply configuration and build client + * + * @return new instance of PolkadotRpcClient + */ + public JavaHttpSubscriptionAdapter build() { + initDefaults(); + return new JavaHttpSubscriptionAdapter(target, httpClient, onClose, rpcCoder); + } } } diff --git a/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/WsResponse.java b/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/WsResponse.java index 07df5b5..b40e181 100644 --- a/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/WsResponse.java +++ b/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/WsResponse.java @@ -5,7 +5,7 @@ /** * Container for the WebSocker message. A message may be a subscription event, or a response to a standard RPC call. * - * @see PolkadotWsApi.SubscriptionResponse + * @see JavaHttpSubscriptionAdapter.SubscriptionResponse * @see RpcResponse */ public class WsResponse { @@ -38,11 +38,11 @@ public Object getValue() { * Make sure the value is SubscriptionResponse and return it * @return value as event */ - public PolkadotWsApi.SubscriptionResponse asEvent() { + public JavaHttpSubscriptionAdapter.SubscriptionResponse asEvent() { if (type != Type.SUBSCRIPTION) { throw new ClassCastException("Not an event"); } - return (PolkadotWsApi.SubscriptionResponse) value; + return (JavaHttpSubscriptionAdapter.SubscriptionResponse) value; } /** @@ -62,7 +62,7 @@ public RpcResponse asRpc() { * @param event event data * @return response instance configured for Subscription Event */ - public static WsResponse subscription(PolkadotWsApi.SubscriptionResponse event) { + public static WsResponse subscription(JavaHttpSubscriptionAdapter.SubscriptionResponse event) { return new WsResponse(Type.SUBSCRIPTION, event); } diff --git a/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/DecodeResponseSpec.groovy b/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/DecodeResponseSpec.groovy index 556aec9..b13aa01 100644 --- a/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/DecodeResponseSpec.groovy +++ b/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/DecodeResponseSpec.groovy @@ -151,7 +151,7 @@ class DecodeResponseSpec extends Specification { def act = decoder.decode(json) then: act.type == WsResponse.Type.SUBSCRIPTION - PolkadotWsApi.SubscriptionResponse event = act.asEvent() + JavaHttpSubscriptionAdapter.SubscriptionResponse event = act.asEvent() event.method == "chain_newHead" event.id == "EsqruyKPnZvPZ6fr" event.value instanceof BlockJson.Header @@ -187,7 +187,7 @@ class DecodeResponseSpec extends Specification { def act = decoder.decode(json) then: act.type == WsResponse.Type.SUBSCRIPTION - PolkadotWsApi.SubscriptionResponse event = act.asEvent() + JavaHttpSubscriptionAdapter.SubscriptionResponse event = act.asEvent() event.method == "chain_newHead" event.id == "EsqruyKPnZvPZ6fr" event.value instanceof BlockJson.Header @@ -224,7 +224,7 @@ class DecodeResponseSpec extends Specification { def act = decoder.decode(json) then: act.type == WsResponse.Type.SUBSCRIPTION - PolkadotWsApi.SubscriptionResponse event = act.asEvent() + JavaHttpSubscriptionAdapter.SubscriptionResponse event = act.asEvent() event.method == "state_storage" event.id == "EKMIn5gSrVmo1cgU" event.value instanceof StorageChangeSetJson diff --git a/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/DefaultSubscriptionSpec.groovy b/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/DefaultSubscriptionSpec.groovy index c000eb6..c0e4aad 100644 --- a/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/DefaultSubscriptionSpec.groovy +++ b/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/DefaultSubscriptionSpec.groovy @@ -1,5 +1,6 @@ package io.emeraldpay.polkaj.apiws +import io.emeraldpay.polkaj.api.PolkadotApi import io.emeraldpay.polkaj.api.RpcCall import io.emeraldpay.polkaj.api.Subscription import spock.lang.Specification @@ -43,24 +44,26 @@ class DefaultSubscriptionSpec extends Specification { def "Close unsubscribes and self-removes"() { setup: - def client = Mock(PolkadotWsApi) + def adapter = Mock(JavaHttpSubscriptionAdapter) + def client = Mock(PolkadotApi) when: - def s = new DefaultSubscription(null, "untest", client) + def s = new DefaultSubscription(null, "untest", adapter) s.setId("EsqruyKPnZvPZ6fr") s.close() then: - 1 * client.execute(RpcCall.create(Boolean.class, "untest", ["EsqruyKPnZvPZ6fr"])) >> CompletableFuture.completedFuture(true) - 1 * client.removeSubscription("EsqruyKPnZvPZ6fr") + 1 * adapter.produceRpcFuture(RpcCall.create(Boolean.class, "untest", ["EsqruyKPnZvPZ6fr"])) >> CompletableFuture.completedFuture(true) + 1 * adapter.removeSubscription("EsqruyKPnZvPZ6fr") } def "Close does nothing if not initialized"() { setup: - def client = Mock(PolkadotWsApi) + def adapter = Mock(JavaHttpSubscriptionAdapter) + def client = Mock(PolkadotApi) when: - def s = new DefaultSubscription(null, "untest", client) + def s = new DefaultSubscription(null, "untest", adapter) s.close() then: 0 * client.execute(_) >> CompletableFuture.completedFuture(false) - 0 * client.removeSubscription(_) + 0 * adapter.removeSubscription(_) } } diff --git a/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/JavaSubscriptionAdapterSpec.groovy b/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/JavaSubscriptionAdapterSpec.groovy new file mode 100644 index 0000000..b18d2b8 --- /dev/null +++ b/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/JavaSubscriptionAdapterSpec.groovy @@ -0,0 +1,79 @@ +package io.emeraldpay.polkaj.apiws + +import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.polkaj.api.MockWsServer +import io.emeraldpay.polkaj.api.PolkadotApi +import io.emeraldpay.polkaj.api.RpcCall +import io.emeraldpay.polkaj.api.RpcCoder +import io.emeraldpay.polkaj.api.SubscriptionAdapter +import io.emeraldpay.polkaj.api.SubscriptionAdapterSpec + +import java.util.concurrent.ExecutorService +import java.util.concurrent.TimeUnit + +class JavaSubscriptionAdapterSpec extends SubscriptionAdapterSpec { + + // needs large timeouts and sleep, especially on CI where it's much slower to run + static TIMEOUT = 15 + static SLEEP = 250 + + @Override + SubscriptionAdapter provideAdapter(String connectTo) { + def adapter = JavaHttpSubscriptionAdapter.newBuilder() + .connectTo(connectTo) + .build() + adapter.connect().get(TIMEOUT, TimeUnit.SECONDS) + return adapter + } + + def "Works with provided RpcCoder"() { + setup: + RpcCoder rpcCoder = Spy(new RpcCoder(new ObjectMapper())) + def adapter = JavaHttpSubscriptionAdapter.newBuilder() + .rpcCoder(rpcCoder) + .connectTo("ws://localhost:${port}") + .build() + adapter.connect().get(TIMEOUT, TimeUnit.SECONDS) + when: + server.onNextReply('{"jsonrpc":"2.0","result":"Hello World!","id":0}') + def f = adapter.produceRpcFuture(RpcCall.create(String.class, "test_foo")) + def act = f.get(TIMEOUT, TimeUnit.SECONDS) + then: + act == "Hello World!" + 1 * rpcCoder.nextId() + } + + def "By default connects to 9944"() { + setup: + server.stop() + println("Start new on 9944") + server = new MockWsServer(9944) + server.start() + Thread.sleep(SLEEP) + def adapter = JavaHttpSubscriptionAdapter.newBuilder() + .build() + adapter.connect().get(TIMEOUT, TimeUnit.SECONDS) + polkadotApi = PolkadotApi.newBuilder().subscriptionAdapter(adapter).rpcCallAdapter(adapter).build() + when: + server.onNextReply('{"jsonrpc":"2.0","result":"Hello World!","id":0}') + def f = polkadotApi.execute(RpcCall.create(String.class, "test_foo")) + def act = f.get(TIMEOUT, TimeUnit.SECONDS) + then: + act == "Hello World!" + } + + def "Works with provided onClose"(){ + setup: + def onClose = Spy(Runnable.class) + def executor = Spy(ExecutorService.class) + def adapter = JavaHttpSubscriptionAdapter.newBuilder() + .executor(executor) + .onClose(onClose) + .build() + when: + adapter.close() + then: + 1 * onClose.run() + } + +} diff --git a/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/WsResponseSpec.groovy b/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/WsResponseSpec.groovy index 6185243..e020a20 100644 --- a/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/WsResponseSpec.groovy +++ b/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/WsResponseSpec.groovy @@ -17,11 +17,11 @@ class WsResponseSpec extends Specification { def "Creates subscription response"() { when: - def act = WsResponse.subscription(new PolkadotWsApi.SubscriptionResponse("EsqruyKPnZvPZ6fr", "test", "test")) + def act = WsResponse.subscription(new JavaHttpSubscriptionAdapter.SubscriptionResponse("EsqruyKPnZvPZ6fr", "test", "test")) then: act.getType() == WsResponse.Type.SUBSCRIPTION - act.getValue() == new PolkadotWsApi.SubscriptionResponse("EsqruyKPnZvPZ6fr", "test", "test") - act.asEvent() == new PolkadotWsApi.SubscriptionResponse("EsqruyKPnZvPZ6fr", "test", "test") + act.getValue() == new JavaHttpSubscriptionAdapter.SubscriptionResponse("EsqruyKPnZvPZ6fr", "test", "test") + act.asEvent() == new JavaHttpSubscriptionAdapter.SubscriptionResponse("EsqruyKPnZvPZ6fr", "test", "test") } def "Cannot cast rcp to event"() { @@ -33,7 +33,7 @@ class WsResponseSpec extends Specification { def "Cannot cast event to rpc"() { when: - WsResponse.subscription(new PolkadotWsApi.SubscriptionResponse("EsqruyKPnZvPZ6fr", "test", "test")).asRpc() + WsResponse.subscription(new JavaHttpSubscriptionAdapter.SubscriptionResponse("EsqruyKPnZvPZ6fr", "test", "test")).asRpc() then: thrown(ClassCastException) } diff --git a/polkaj-common-types/build.gradle b/polkaj-common-types/build.gradle index 252249c..77c8cd4 100644 --- a/polkaj-common-types/build.gradle +++ b/polkaj-common-types/build.gradle @@ -1,3 +1,5 @@ +apply from: '../common_java_app.gradle' + dependencies { api project(":polkaj-ss58") } \ No newline at end of file diff --git a/polkaj-json-types/build.gradle b/polkaj-json-types/build.gradle index 236b3b3..72e5c40 100644 --- a/polkaj-json-types/build.gradle +++ b/polkaj-json-types/build.gradle @@ -1,3 +1,5 @@ +apply from: '../common_java_app.gradle' + dependencies { api project(":polkaj-common-types") api project(":polkaj-ss58") diff --git a/polkaj-scale-types/build.gradle b/polkaj-scale-types/build.gradle index ea8fcf3..f4596dc 100644 --- a/polkaj-scale-types/build.gradle +++ b/polkaj-scale-types/build.gradle @@ -1,3 +1,5 @@ +apply from: '../common_java_app.gradle' + dependencies { api project(":polkaj-scale") api project(":polkaj-common-types") diff --git a/polkaj-scale/build.gradle b/polkaj-scale/build.gradle index 81b247b..64cdaea 100644 --- a/polkaj-scale/build.gradle +++ b/polkaj-scale/build.gradle @@ -1,3 +1,5 @@ +apply from: '../common_java_app.gradle' + dependencies { api project(":polkaj-common-types") } \ No newline at end of file diff --git a/polkaj-schnorrkel/build.gradle b/polkaj-schnorrkel/build.gradle index 65c24d4..f959f14 100644 --- a/polkaj-schnorrkel/build.gradle +++ b/polkaj-schnorrkel/build.gradle @@ -1,3 +1,5 @@ +apply from: '../common_java_app.gradle' + dependencies { } @@ -25,4 +27,4 @@ jar { into "native/windows" include '*.dll' } -} \ No newline at end of file +} diff --git a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java b/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java index 5182ab1..732f0ee 100644 --- a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java +++ b/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java @@ -1,12 +1,7 @@ package io.emeraldpay.polkaj.schnorrkel; -import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.lang.invoke.VarHandle; import java.nio.file.Files; import java.nio.file.Path; import java.security.NoSuchAlgorithmException; @@ -107,22 +102,22 @@ private static byte[] encodeKeyPair(Schnorrkel.KeyPair keyPair) { private static final String LIBNAME = "polkaj_schnorrkel"; static { + try { // JVM needs native libraries to be loaded from filesystem, so first we need to extract - // files for current OS into a temp dir - extractJNI(); + // files for current OS into a temp dir then load the file. + if(!extractAndLoadJNI()) { + // load the native library, this is for running tests + System.loadLibrary(LIBNAME); + } } catch (IOException e) { System.err.println("Failed to extract JNI library from Jar file. " + e.getClass() + ":" + e.getMessage()); - } - try { - // load the native library - System.loadLibrary(LIBNAME); - } catch (UnsatisfiedLinkError e) { + } catch (UnsatisfiedLinkError e){ System.err.println("Failed to load native library. Polkaj Schnorrkel methods are unavailable. Error: " + e.getMessage()); } } - private static void extractJNI() throws IOException { + private static boolean extractAndLoadJNI() throws IOException { // define which of files bundled with Jar to extract String os = System.getProperty("os.name", "unknown").toLowerCase(); if (os.contains("win")) { @@ -133,47 +128,29 @@ private static void extractJNI() throws IOException { os = "linux"; } else { System.err.println("Unknown OS: " + os + ". Unable to setup native library for Polkaj Schnorrkel"); - return; + return false; } String filename = System.mapLibraryName(LIBNAME); String classpathFile = "/native/" + os + "/" + filename; // extract native lib to the filesystem InputStream lib = Schnorrkel.class.getResourceAsStream(classpathFile); + System.out.println(classpathFile); if (lib == null) { System.err.println("Library " + classpathFile + " is not found in the classpath"); - return; + return false; } Path dir = Files.createTempDirectory(LIBNAME); Path target = dir.resolve(filename); + Files.copy(lib, target); + System.load(target.toFile().getAbsolutePath()); + System.out.println("library " + classpathFile + " is loaded"); // setup JVM to delete files on exit, when possible target.toFile().deleteOnExit(); dir.toFile().deleteOnExit(); - - // prepare new path to native libraries, including the directly with just extracted file - final String libraryPathProperty = "java.library.path"; - String userLibs = System.getProperty(libraryPathProperty); - if (userLibs == null || "".equals(userLibs)) { - userLibs = dir.toAbsolutePath().toString(); - } else { - userLibs = userLibs + File.pathSeparatorChar + dir.toAbsolutePath().toString(); - } - - // Update paths to search for native libraries - System.setProperty(libraryPathProperty, userLibs); - // But since it may be already processed and cached we need to update the current value - try { - MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(ClassLoader.class, MethodHandles.lookup()); - VarHandle usrPathsField = lookup.findStaticVarHandle(ClassLoader.class, - "usr_paths", String[].class); - MethodHandle initializePathMethod = lookup.findStatic(ClassLoader.class, - "initializePath", MethodType.methodType(String[].class, String.class)); - usrPathsField.set(initializePathMethod.invoke(libraryPathProperty)); - } catch (Throwable e) { - System.err.println("Unable to update usr_paths field. " + e.getClass() + ":" + e.getMessage()); - } + return true; } } diff --git a/polkaj-ss58/build.gradle b/polkaj-ss58/build.gradle index c9c9093..42f765a 100644 --- a/polkaj-ss58/build.gradle +++ b/polkaj-ss58/build.gradle @@ -1,3 +1,5 @@ +apply from: '../common_java_app.gradle' + dependencies { api 'com.github.multiformats:java-multibase:v1.0.0' implementation 'org.bouncycastle:bcprov-jdk15on:1.65' diff --git a/polkaj-tx/build.gradle b/polkaj-tx/build.gradle index d6d231b..b57bf4c 100644 --- a/polkaj-tx/build.gradle +++ b/polkaj-tx/build.gradle @@ -1,3 +1,5 @@ +apply from: '../common_java_app.gradle' + dependencies { api project(":polkaj-common-types") api project(":polkaj-scale-types") diff --git a/settings.gradle b/settings.gradle index 9836ba8..333c1ae 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,5 +9,6 @@ include "polkaj-ss58", "polkaj-api-base", "polkaj-api-http", "polkaj-api-ws", - "polkaj-tx" + "polkaj-tx", + "polkaj-adapter-tests"