From 9d6127b02a7b116206a659a8404cae7d6d379406 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Wed, 8 Sep 2021 10:24:38 -0500 Subject: [PATCH 01/38] add android aar artifact for schnorrkel supporting arm, arm64, x86 and x86_64 --- .gitignore | 2 + polkaj-schnorrkel-android/.gitignore | 1 + polkaj-schnorrkel-android/build.gradle | 70 +++++++++++++++++++ .../src/main/AndroidManifest.xml | 9 +++ .../polkaj/schnorrkel/Schnorrkel.java | 11 --- .../schnorrkel/SchnorrkelException.java | 0 .../polkaj/schnorrkel/SchnorrkelNative.java | 3 +- .../src/rust/.gitignore | 0 .../src/rust/Cargo.toml | 2 +- .../src/rust/src/lib.rs | 0 polkaj-schnorrkel/build.gradle | 12 +++- polkaj-tx/build.gradle | 2 +- .../emeraldpay/polkaj/tx/ExtrinsicSigner.java | 2 +- settings.gradle | 2 + 14 files changed, 99 insertions(+), 17 deletions(-) create mode 100644 polkaj-schnorrkel-android/.gitignore create mode 100644 polkaj-schnorrkel-android/build.gradle create mode 100644 polkaj-schnorrkel-android/src/main/AndroidManifest.xml rename {polkaj-schnorrkel => polkaj-schnorrkel-common}/src/main/java/io/emeraldpay/polkaj/schnorrkel/Schnorrkel.java (95%) rename {polkaj-schnorrkel => polkaj-schnorrkel-common}/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelException.java (100%) rename {polkaj-schnorrkel => polkaj-schnorrkel-common}/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java (98%) rename {polkaj-schnorrkel => polkaj-schnorrkel-common}/src/rust/.gitignore (100%) rename {polkaj-schnorrkel => polkaj-schnorrkel-common}/src/rust/Cargo.toml (83%) rename {polkaj-schnorrkel => polkaj-schnorrkel-common}/src/rust/src/lib.rs (100%) diff --git a/.gitignore b/.gitignore index f87b4bc..aa177d4 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ scripts .gradle build + +local.properties \ No newline at end of file diff --git a/polkaj-schnorrkel-android/.gitignore b/polkaj-schnorrkel-android/.gitignore new file mode 100644 index 0000000..f443164 --- /dev/null +++ b/polkaj-schnorrkel-android/.gitignore @@ -0,0 +1 @@ +src/main/jniLibs \ No newline at end of file diff --git a/polkaj-schnorrkel-android/build.gradle b/polkaj-schnorrkel-android/build.gradle new file mode 100644 index 0000000..07502e7 --- /dev/null +++ b/polkaj-schnorrkel-android/build.gradle @@ -0,0 +1,70 @@ +import java.nio.file.Paths + +buildscript { + repositories { + google() + maven { + url "https://plugins.gradle.org/m2/" + } + mavenCentral() + } + dependencies { + classpath "com.android.tools.build:gradle:4.2.2" + classpath "gradle.plugin.com.github.willir.rust:plugin:0.3.4" + } +} + +apply plugin: 'com.android.library' +apply plugin: "com.github.willir.rust.cargo-ndk-android" + +android { + ndkVersion System.getenv("ANDROID_NDK_VERSION") + + compileSdkVersion 30 + + defaultConfig { + minSdkVersion 24 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + sourceSets { + main { + java { + srcDirs = ["../polkaj-schnorrkel-common/src/main/java"] + } + } + } +} + +dependencies { +} + +repositories { + google() +} + +clean.doFirst { + delete "src/main/jniLibs" +} + +cargoNdk { + def rustDir = "${rootDir.absolutePath}/polkaj-schnorrkel-common/src/rust" + def cargoTarget = "$buildDir/rust" + def relativeCargoTarget = Paths.get(rustDir).relativize(Paths.get(cargoTarget)) + + module = "polkaj-schnorrkel-common/src/rust" + targetDirectory = relativeCargoTarget.toString() + extraCargoBuildArguments = ["--target-dir=$relativeCargoTarget"] + verbose = true +} \ No newline at end of file diff --git a/polkaj-schnorrkel-android/src/main/AndroidManifest.xml b/polkaj-schnorrkel-android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..41e71fa --- /dev/null +++ b/polkaj-schnorrkel-android/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/Schnorrkel.java b/polkaj-schnorrkel-common/src/main/java/io/emeraldpay/polkaj/schnorrkel/Schnorrkel.java similarity index 95% rename from polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/Schnorrkel.java rename to polkaj-schnorrkel-common/src/main/java/io/emeraldpay/polkaj/schnorrkel/Schnorrkel.java index 0ce82b1..791c55c 100644 --- a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/Schnorrkel.java +++ b/polkaj-schnorrkel-common/src/main/java/io/emeraldpay/polkaj/schnorrkel/Schnorrkel.java @@ -1,18 +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; import java.security.SecureRandom; import java.util.Arrays; -import java.util.logging.MemoryHandler; /** * Schnorrkel implements Schnorr signature on Ristretto compressed Ed25519 points, as well as related protocols like diff --git a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelException.java b/polkaj-schnorrkel-common/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelException.java similarity index 100% rename from polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelException.java rename to polkaj-schnorrkel-common/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelException.java diff --git a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java b/polkaj-schnorrkel-common/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java similarity index 98% rename from polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java rename to polkaj-schnorrkel-common/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java index 732f0ee..62bc63a 100644 --- a/polkaj-schnorrkel/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java +++ b/polkaj-schnorrkel-common/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java @@ -107,7 +107,7 @@ private static byte[] encodeKeyPair(Schnorrkel.KeyPair keyPair) { // JVM needs native libraries to be loaded from filesystem, so first we need to extract // files for current OS into a temp dir then load the file. if(!extractAndLoadJNI()) { - // load the native library, this is for running tests + // load the native library, this is for running tests and android System.loadLibrary(LIBNAME); } } catch (IOException e) { @@ -119,6 +119,7 @@ private static byte[] encodeKeyPair(Schnorrkel.KeyPair keyPair) { private static boolean extractAndLoadJNI() throws IOException { // define which of files bundled with Jar to extract + if(System.getProperty("java.runtime.name", "unknown").contains("android")) return false; String os = System.getProperty("os.name", "unknown").toLowerCase(); if (os.contains("win")) { os = "windows"; diff --git a/polkaj-schnorrkel/src/rust/.gitignore b/polkaj-schnorrkel-common/src/rust/.gitignore similarity index 100% rename from polkaj-schnorrkel/src/rust/.gitignore rename to polkaj-schnorrkel-common/src/rust/.gitignore diff --git a/polkaj-schnorrkel/src/rust/Cargo.toml b/polkaj-schnorrkel-common/src/rust/Cargo.toml similarity index 83% rename from polkaj-schnorrkel/src/rust/Cargo.toml rename to polkaj-schnorrkel-common/src/rust/Cargo.toml index daa3d77..1e2c55d 100644 --- a/polkaj-schnorrkel/src/rust/Cargo.toml +++ b/polkaj-schnorrkel-common/src/rust/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "polkaj-schnorrkel" +name = "polkaj_schnorrkel" version = "0.3.0" [lib] diff --git a/polkaj-schnorrkel/src/rust/src/lib.rs b/polkaj-schnorrkel-common/src/rust/src/lib.rs similarity index 100% rename from polkaj-schnorrkel/src/rust/src/lib.rs rename to polkaj-schnorrkel-common/src/rust/src/lib.rs diff --git a/polkaj-schnorrkel/build.gradle b/polkaj-schnorrkel/build.gradle index f959f14..1b78943 100644 --- a/polkaj-schnorrkel/build.gradle +++ b/polkaj-schnorrkel/build.gradle @@ -3,13 +3,21 @@ apply from: '../common_java_app.gradle' dependencies { } +sourceSets { + main { + java { + srcDirs = ["../polkaj-schnorrkel-common/src/main/java"] + } + } +} + test { systemProperty "java.library.path", file("${buildDir}/rust/release").absolutePath } task compileRust(type:Exec) { - workingDir 'src/rust' - commandLine 'cargo', 'build', '--release', '--target-dir=../../build/rust' + workingDir "../polkaj-schnorrkel-common/src/rust" + commandLine 'cargo', 'build', '--release', "--target-dir=${buildDir}/rust" } compileJava.dependsOn(compileRust) diff --git a/polkaj-tx/build.gradle b/polkaj-tx/build.gradle index b57bf4c..8848912 100644 --- a/polkaj-tx/build.gradle +++ b/polkaj-tx/build.gradle @@ -5,7 +5,7 @@ dependencies { api project(":polkaj-scale-types") api project(":polkaj-json-types") api project(":polkaj-ss58") - api project(":polkaj-schnorrkel") + implementation project(":polkaj-schnorrkel") api project(":polkaj-api-base") // for xxHash diff --git a/polkaj-tx/src/main/java/io/emeraldpay/polkaj/tx/ExtrinsicSigner.java b/polkaj-tx/src/main/java/io/emeraldpay/polkaj/tx/ExtrinsicSigner.java index 4f599fe..7911a53 100644 --- a/polkaj-tx/src/main/java/io/emeraldpay/polkaj/tx/ExtrinsicSigner.java +++ b/polkaj-tx/src/main/java/io/emeraldpay/polkaj/tx/ExtrinsicSigner.java @@ -136,7 +136,7 @@ private boolean isValidEd25519Signature(byte[] payload, Extrinsic.Signature sign PublicKey publicKey = keyFactory.generatePublic(keySpec); // verify signature - final var signedData = Signature.getInstance("ed25519", bouncyCastleProvider); + final Signature signedData = Signature.getInstance("ed25519", bouncyCastleProvider); signedData.initVerify(publicKey); signedData.update(payload); return signedData.verify(signature.getValue().getBytes()); diff --git a/settings.gradle b/settings.gradle index 333c1ae..299dcaf 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,6 +5,8 @@ include "polkaj-ss58", "polkaj-json-types", "polkaj-scale-types", "polkaj-schnorrkel", + "polkaj-schnorrkel-common", + "polkaj-schnorrkel-android", "polkaj-common-types", "polkaj-api-base", "polkaj-api-http", From a8259e8d06c44dda95fe8115cfc0a5b7e4375d86 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Thu, 9 Sep 2021 19:29:12 -0500 Subject: [PATCH 02/38] publish android artifact --- README.adoc | 2 +- polkaj-schnorrkel-android/build.gradle | 63 +++++++++++++++++++ .../src/main/AndroidManifest.xml | 7 +-- polkaj-tx/build.gradle | 2 +- 4 files changed, 66 insertions(+), 8 deletions(-) diff --git a/README.adoc b/README.adoc index 028f3a0..bde2521 100644 --- a/README.adoc +++ b/README.adoc @@ -34,7 +34,7 @@ To use development SNAPSHOT versions you need to install the library into the lo .Install into local Maven ---- -gradle install +gradle publishToMavenLocal ---- .Using with Gradle diff --git a/polkaj-schnorrkel-android/build.gradle b/polkaj-schnorrkel-android/build.gradle index 07502e7..87c7de0 100644 --- a/polkaj-schnorrkel-android/build.gradle +++ b/polkaj-schnorrkel-android/build.gradle @@ -16,6 +16,8 @@ buildscript { apply plugin: 'com.android.library' apply plugin: "com.github.willir.rust.cargo-ndk-android" +apply plugin: 'maven-publish' +apply plugin: 'com.jfrog.bintray' android { ndkVersion System.getenv("ANDROID_NDK_VERSION") @@ -67,4 +69,65 @@ cargoNdk { targetDirectory = relativeCargoTarget.toString() extraCargoBuildArguments = ["--target-dir=$relativeCargoTarget"] verbose = true +} + +afterEvaluate { + publishing { + publications { + // Creates a Maven publication called "release". + mavenAndroid(MavenPublication) { + // Applies the component for the release build variant. + from components.release + + // You can then customize attributes of the publication as shown below. + groupId = project.group + artifactId = project.name + version = project.version + + 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 = ['mavenAndroid'] + + 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/polkaj-schnorrkel-android/src/main/AndroidManifest.xml b/polkaj-schnorrkel-android/src/main/AndroidManifest.xml index 41e71fa..e849c2e 100644 --- a/polkaj-schnorrkel-android/src/main/AndroidManifest.xml +++ b/polkaj-schnorrkel-android/src/main/AndroidManifest.xml @@ -1,9 +1,4 @@ - - - - - + \ No newline at end of file diff --git a/polkaj-tx/build.gradle b/polkaj-tx/build.gradle index 8848912..75a42ed 100644 --- a/polkaj-tx/build.gradle +++ b/polkaj-tx/build.gradle @@ -5,7 +5,7 @@ dependencies { api project(":polkaj-scale-types") api project(":polkaj-json-types") api project(":polkaj-ss58") - implementation project(":polkaj-schnorrkel") + compileOnly project(":polkaj-schnorrkel") api project(":polkaj-api-base") // for xxHash From e881bc894029f90ec5495c4f3edc044aa04b940b Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Fri, 10 Sep 2021 17:07:07 -0500 Subject: [PATCH 03/38] add missing test impl --- polkaj-tx/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/polkaj-tx/build.gradle b/polkaj-tx/build.gradle index 75a42ed..8b8c7e7 100644 --- a/polkaj-tx/build.gradle +++ b/polkaj-tx/build.gradle @@ -11,4 +11,6 @@ dependencies { // for xxHash api 'net.openhft:zero-allocation-hashing:0.11' api 'org.bouncycastle:bcprov-jdk15on:1.65' + + testImplementation project(":polkaj-schnorrkel") } \ No newline at end of file From 811323dd2809e79ffa0dd37e71e14066b60f72d1 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sat, 11 Sep 2021 15:48:27 -0500 Subject: [PATCH 04/38] remove java 9 api --- .../src/main/java/io/emeraldpay/polkaj/scale/CompactMode.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/polkaj-scale/src/main/java/io/emeraldpay/polkaj/scale/CompactMode.java b/polkaj-scale/src/main/java/io/emeraldpay/polkaj/scale/CompactMode.java index acc5d7b..4268419 100644 --- a/polkaj-scale/src/main/java/io/emeraldpay/polkaj/scale/CompactMode.java +++ b/polkaj-scale/src/main/java/io/emeraldpay/polkaj/scale/CompactMode.java @@ -9,9 +9,9 @@ public enum CompactMode { FOUR((byte)0b10), BIGINT((byte)0b11); - private static BigInteger MAX = BigInteger.TWO.pow(536).subtract(BigInteger.ONE); + final private static BigInteger MAX = BigInteger.valueOf(2).pow(536).subtract(BigInteger.ONE); - private byte value; + final private byte value; private CompactMode(byte value) { this.value = value; From bb710c2acacbfa5dbb2b8e4d2c87ab43b5e6faac Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sat, 11 Sep 2021 15:49:45 -0500 Subject: [PATCH 05/38] Add friendly dependency reminder --- .../src/main/java/io/emeraldpay/polkaj/tx/ExtrinsicSigner.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/polkaj-tx/src/main/java/io/emeraldpay/polkaj/tx/ExtrinsicSigner.java b/polkaj-tx/src/main/java/io/emeraldpay/polkaj/tx/ExtrinsicSigner.java index 7911a53..59f8a4c 100644 --- a/polkaj-tx/src/main/java/io/emeraldpay/polkaj/tx/ExtrinsicSigner.java +++ b/polkaj-tx/src/main/java/io/emeraldpay/polkaj/tx/ExtrinsicSigner.java @@ -119,6 +119,9 @@ public boolean isValid(ExtrinsicContext ctx, CALL call, Extrinsic.Signature sign catch (SchnorrkelException | SignatureException | NoSuchAlgorithmException | InvalidKeySpecException | IOException | InvalidKeyException e) { throw new SignException("Failed to verify", e); } + catch(NoClassDefFoundError e){ + throw new SignException("Schnorrkel library not found. Did you forget to include it as a dependency?"); + } } private boolean isValidEd25519Signature(byte[] payload, Extrinsic.Signature signature, Address address) From 83449c907c5ae56fe0ef382833dade3f632cee19 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sat, 11 Sep 2021 16:30:31 -0500 Subject: [PATCH 06/38] remove java 9 and 11 apis --- common_java_app.gradle | 2 - .../io/emeraldpay/polkaj/api/RpcCoder.java | 1 + .../polkaj/api/StandardCommands.java | 163 +----------------- .../polkaj/api/StandardSubscriptions.java | 2 +- .../polkaj/scale/ScaleCodecWriter.java | 2 +- .../scale/writer/BoolOptionalWriter.java | 2 +- 6 files changed, 6 insertions(+), 166 deletions(-) diff --git a/common_java_app.gradle b/common_java_app.gradle index 5174467..94ac28e 100644 --- a/common_java_app.gradle +++ b/common_java_app.gradle @@ -1,6 +1,5 @@ apply plugin: 'java' apply plugin: 'java-library' -apply plugin: 'maven' apply plugin: 'maven-publish' apply plugin: 'groovy' apply plugin: 'jacoco' @@ -12,7 +11,6 @@ afterEvaluate { rootProject.tasks.coverageReport.classDirectories.setFrom rootProject.tasks.coverageReport.classDirectories.files + files(it.sourceSets.main.output) } - tasks.withType(JavaCompile) { options.debug = true } diff --git a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/RpcCoder.java b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/RpcCoder.java index 41bc2fd..1335033 100644 --- a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/RpcCoder.java +++ b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/RpcCoder.java @@ -72,6 +72,7 @@ final public T decode(int id, String content, JavaType clazz) { /** * Encode RPC request as JSON * + * @param type of returned object * @param id id of the request * @param call the RpcCall to encode * @return full JSON of the request diff --git a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/StandardCommands.java b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/StandardCommands.java index 88b8f82..b9c3ff1 100644 --- a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/StandardCommands.java +++ b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/StandardCommands.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; /** @@ -126,174 +127,14 @@ public RpcCall stateGetStorage(ByteData key) { return RpcCall.create(ByteData.class, PolkadotMethod.STATE_GET_STORAGE, key.toString()); } - /** - * @deprecated Use RpcCall.create(Hash256.class, PolkadotMethod.STATE_GET_STORAGE_HASH, ...) - * @param key key - * @param at block (optional) - * @return command - */ - @Deprecated(forRemoval = true) - public RpcCall stateGetStorageHash(ByteData key, Hash256 at) { - List params = at == null ? List.of(key) : List.of(key, at); - return RpcCall.create(Hash256.class, PolkadotMethod.STATE_GET_STORAGE_HASH, params); - } - - /** - * @deprecated Use RpcCall.create(Hash256.class, PolkadotMethod.STATE_GET_STORAGE_SIZE, ...) - * @param key key - * @param at block (optional) - * @return command - */ - @Deprecated(forRemoval = true) - public RpcCall stateGetStorageSize(ByteData key, Hash256 at) { - List params = at == null ? List.of(key) : List.of(key, at); - return RpcCall.create(Long.class, PolkadotMethod.STATE_GET_STORAGE_SIZE, params); - } - - /** - * @deprecated Use RpcCall.create(ByteData.class, PolkadotMethod.STATE_CALL, ...) - * @param method method name - * @param data call data - * @param at block (optional) - * @return command - */ - @Deprecated(forRemoval = true) - public RpcCall stateCall(String method, ByteData data, Hash256 at) { - List params = at == null ? List.of(method, data) : List.of(method, data, at); - return RpcCall.create(ByteData.class, PolkadotMethod.STATE_CALL, params); - } - - /** - * @deprecated Use RpcCall.create(ByteData.class, "state_getChildKeys", ...).expectList() - * @param childStorageKey child key - * @param childDefinition child definition - * @param childType type - * @param key key - * @param at block (optional) - * @return command - */ - @Deprecated(forRemoval = true) - public RpcCall> stateGetChildKeys(ByteData childStorageKey, ByteData childDefinition, long childType, ByteData key, Hash256 at) { - List params = new ArrayList<>(List.of(childStorageKey, childDefinition, childType, key)); - if (at != null) { - params.add(at); - } - return RpcCall.create(ByteData.class, "state_getChildKeys", params).expectList(); - } - - /** - * @deprecated Use RpcCall.create(ByteData.class, "state_getChildStorage", ...) - * @param childStorageKey child key - * @param childDefinition child definition - * @param childType type - * @param key key - * @param at block (optional) - * @return command - */ - @Deprecated(forRemoval = true) - public RpcCall stateGetStorageData(ByteData childStorageKey, ByteData childDefinition, long childType, ByteData key, Hash256 at) { - List params = new ArrayList<>(List.of(childStorageKey, childDefinition, childType, key)); - if (at != null) { - params.add(at); - } - return RpcCall.create(ByteData.class, "state_getChildStorage", params); - } - - /** - * @deprecated Use RpcCall.create(Hash256.class, "state_getChildStorageHash", ...) - * @param childStorageKey child key - * @param childDefinition child definition - * @param childType type - * @param key key - * @param at block (optional) - * @return command - */ - @Deprecated(forRemoval = true) - public RpcCall stateGetChildStorageHash(ByteData childStorageKey, ByteData childDefinition, long childType, ByteData key, Hash256 at) { - List params = new ArrayList<>(List.of(childStorageKey, childDefinition, childType, key)); - if (at != null) { - params.add(at); - } - return RpcCall.create(Hash256.class, "state_getChildStorageHash", params); - } - - /** - * @deprecated RpcCall.create(Long.class, "state_getChildStorageSize", ...) - * @param childStorageKey child key - * @param childDefinition child definition - * @param childType type - * @param key key - * @param at block (optional) - * @return command - */ - @Deprecated(forRemoval = true) - public RpcCall stateGetChildStorageSize(ByteData childStorageKey, ByteData childDefinition, long childType, ByteData key, Hash256 at) { - List params = new ArrayList<>(List.of(childStorageKey, childDefinition, childType, key)); - if (at != null) { - params.add(at); - } - return RpcCall.create(Long.class, "state_getChildStorageSize", params); - } - - /** - * @deprecated Use RpcCall.create(ByteData.class, PolkadotMethod.STATE_KEYS_PAGED, ...).expectList() - * @param key key - * @param count count - * @param startKey start key (optional) - * @param at block (optional) - * @return command - */ - @Deprecated(forRemoval = true) - public RpcCall> stateGetKeys(ByteData key, int count, ByteData startKey, Hash256 at) { - List params = new ArrayList<>(List.of(key, count)); - if (startKey != null) { - params.add(startKey); - } - if (at != null) { - params.add(at); - } - return RpcCall.create(ByteData.class, PolkadotMethod.STATE_KEYS_PAGED, params).expectList(); - } - public RpcCall stateGetReadProof(List keys, Hash256 at) { - List params = new ArrayList<>(List.of(keys)); + List params = new ArrayList<>(Collections.unmodifiableList(keys)); if (at != null) { params.add(at); } return RpcCall.create(ReadProofJson.class, PolkadotMethod.STATE_GET_READ_PROOF, params); } - /** - * @deprecated Use RpcCall.create(ByteData.class, PolkadotMethod.STATE_QUERY_STORAGE, ...).expectList() - * @param keys keys - * @param fromBlock from block - * @param toBlock to block - * @return command - */ - @Deprecated(forRemoval = true) - public RpcCall> stateQueryStorage(List keys, Hash256 fromBlock, Hash256 toBlock) { - List params = new ArrayList<>(List.of(keys, fromBlock)); - if (toBlock != null) { - params.add(toBlock); - } - return RpcCall.create(ByteData.class, PolkadotMethod.STATE_QUERY_STORAGE, params).expectList(); - } - - /** - * @deprecated Use RpcCall.create(ByteData.class, PolkadotMethod.STATE_QUERY_STORAGE_AT, ...).expectList() - * @param keys keys - * @param at block - * @return command - */ - @Deprecated(forRemoval = true) - public RpcCall> stateQueryStorageAt(List keys, Hash256 at) { - List params = new ArrayList<>(List.of(keys)); - if (at != null) { - params.add(at); - } - return RpcCall.create(ByteData.class, PolkadotMethod.STATE_QUERY_STORAGE_AT, params).expectList(); - } - /** * Request data from storage * @param request key (depending on the storage) diff --git a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/StandardSubscriptions.java b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/StandardSubscriptions.java index f10f1f0..a565448 100644 --- a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/StandardSubscriptions.java +++ b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/StandardSubscriptions.java @@ -57,7 +57,7 @@ public SubscribeCall storage(List keys) { if (keys.isEmpty()) { return storage(); } - return SubscribeCall.create(StorageChangeSetJson.class, PolkadotMethod.STATE_SUBSCRIBE_STORAGE, PolkadotMethod.STATE_UNSUBSCRIBE_STORAGE, List.of(keys)); + return SubscribeCall.create(StorageChangeSetJson.class, PolkadotMethod.STATE_SUBSCRIBE_STORAGE, PolkadotMethod.STATE_UNSUBSCRIBE_STORAGE, Collections.unmodifiableList(keys)); } public SubscribeCall storage(ByteData key) { diff --git a/polkaj-scale/src/main/java/io/emeraldpay/polkaj/scale/ScaleCodecWriter.java b/polkaj-scale/src/main/java/io/emeraldpay/polkaj/scale/ScaleCodecWriter.java index e01c355..b8b0106 100644 --- a/polkaj-scale/src/main/java/io/emeraldpay/polkaj/scale/ScaleCodecWriter.java +++ b/polkaj-scale/src/main/java/io/emeraldpay/polkaj/scale/ScaleCodecWriter.java @@ -123,7 +123,7 @@ public void writeOptional(ScaleWriter writer, Optional value) throws I if (writer instanceof BoolOptionalWriter || writer instanceof BoolWriter) { BOOL_OPT.write(this, (Optional) value); } else { - if (value.isEmpty()) { + if (!value.isPresent()) { BOOL.write(this, false); } else { BOOL.write(this, true); diff --git a/polkaj-scale/src/main/java/io/emeraldpay/polkaj/scale/writer/BoolOptionalWriter.java b/polkaj-scale/src/main/java/io/emeraldpay/polkaj/scale/writer/BoolOptionalWriter.java index 5afd498..ce4f886 100644 --- a/polkaj-scale/src/main/java/io/emeraldpay/polkaj/scale/writer/BoolOptionalWriter.java +++ b/polkaj-scale/src/main/java/io/emeraldpay/polkaj/scale/writer/BoolOptionalWriter.java @@ -10,7 +10,7 @@ public class BoolOptionalWriter implements ScaleWriter> { @Override public void write(ScaleCodecWriter wrt, Optional value) throws IOException { - if (value.isEmpty()) { + if (!value.isPresent()) { wrt.directWrite(0); } else if (value.get()) { wrt.directWrite(2); From 6fcd5f78bac43330c474c6478fed4b9fb485c8cd Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sun, 12 Sep 2021 19:42:47 -0500 Subject: [PATCH 07/38] Add okhttp rpc adaper --- common_java_app.gradle | 2 +- .../polkaj/api/RpcAdapterSpec.groovy | 2 +- .../io/emeraldpay/polkaj/api/RpcCoder.java | 27 +++- .../polkaj/apihttp/JavaHttpAdapter.java | 13 +- polkaj-api-okhttp/build.gradle | 42 +++++ .../polkaj/apiokhttp/OkHttpRpcAdapter.kt | 152 ++++++++++++++++++ .../test/groovy/OkHttpRpcAdapterSpec.groovy | 17 ++ settings.gradle | 1 + 8 files changed, 252 insertions(+), 4 deletions(-) create mode 100644 polkaj-api-okhttp/build.gradle create mode 100644 polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt create mode 100644 polkaj-api-okhttp/src/test/groovy/OkHttpRpcAdapterSpec.groovy diff --git a/common_java_app.gradle b/common_java_app.gradle index 94ac28e..de4b88b 100644 --- a/common_java_app.gradle +++ b/common_java_app.gradle @@ -77,7 +77,7 @@ artifacts { } jacoco { - toolVersion = "0.8.5" + toolVersion = "0.8.7" } publishing { diff --git a/polkaj-adapter-tests/src/main/groovy/io/emeraldpay/polkaj/api/RpcAdapterSpec.groovy b/polkaj-adapter-tests/src/main/groovy/io/emeraldpay/polkaj/api/RpcAdapterSpec.groovy index 779eef2..3b99add 100644 --- a/polkaj-adapter-tests/src/main/groovy/io/emeraldpay/polkaj/api/RpcAdapterSpec.groovy +++ b/polkaj-adapter-tests/src/main/groovy/io/emeraldpay/polkaj/api/RpcAdapterSpec.groovy @@ -108,7 +108,7 @@ abstract class RpcAdapterSpec extends Specification{ then: def t = thrown(ExecutionException) - t.cause instanceof HttpTimeoutException + t.cause instanceof InterruptedIOException } diff --git a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/RpcCoder.java b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/RpcCoder.java index 1335033..562fa60 100644 --- a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/RpcCoder.java +++ b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/RpcCoder.java @@ -4,8 +4,11 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.io.InputStream; import java.util.concurrent.CompletionException; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; public class RpcCoder { @@ -47,14 +50,36 @@ public JavaType responseType(Class resultClazz) { * @throws CompletionException with RpcException details to let executor know that the response is invalid */ final public T decode(int id, String content, JavaType clazz) { + return decodeInternal(id, content, clazz); + } + + /** + * Decode JSON RPC response + * + * @param id expected id + * @param content full JSON content + * @param clazz expected JavaType for the result field + * @param returning type + * @return The decoded result + * @throws CompletionException with RpcException details to let executor know that the response is invalid or an IOException + */ + final public T decode(int id, InputStream content, JavaType clazz){ + return decodeInternal(id, content, clazz); + } + + private T decodeInternal(int id, Object content, JavaType clazz) { JavaType type = objectMapper.getTypeFactory().constructParametricType(RpcResponse.class, clazz); RpcResponse response; try { - response = objectMapper.readerFor(type).readValue(content); + if(content instanceof String) response = objectMapper.readerFor(type).readValue((String)content); + else if(content instanceof InputStream) response = objectMapper.readerFor(type).readValue((InputStream) content); + else throw new IllegalArgumentException("Unsupported content type."); } catch (JsonProcessingException e) { throw new CompletionException( new RpcException(-32603, "Server returned invalid JSON", e) ); + } catch (IOException e){ + throw new CompletionException(e); } if (id != response.getId()) { throw new CompletionException( diff --git a/polkaj-api-http/src/main/java/io/emeraldpay/polkaj/apihttp/JavaHttpAdapter.java b/polkaj-api-http/src/main/java/io/emeraldpay/polkaj/apihttp/JavaHttpAdapter.java index 238e15a..9c931d3 100644 --- a/polkaj-api-http/src/main/java/io/emeraldpay/polkaj/apihttp/JavaHttpAdapter.java +++ b/polkaj-api-http/src/main/java/io/emeraldpay/polkaj/apihttp/JavaHttpAdapter.java @@ -6,17 +6,20 @@ import io.emeraldpay.polkaj.api.*; import io.emeraldpay.polkaj.json.jackson.PolkadotModule; +import java.io.InterruptedIOException; import java.net.URI; import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.net.http.HttpTimeoutException; import java.time.Duration; import java.util.Base64; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.function.Function; /** * Default JSON RPC HTTP client for Polkadot API. It uses Java 11 HttpClient implementation for requests. @@ -50,7 +53,7 @@ private JavaHttpAdapter(URI target, HttpClient httpClient, String basicAuth, Dur HttpRequest.Builder request = HttpRequest.newBuilder() .uri(target) .timeout(timeout) - .header("User-Agent", "Polkaj/0.3") //TODO generate version during compilation + .header("User-Agent", "Polkaj/java/0.5") //TODO generate version during compilation .header("Content-Type", APPLICATION_JSON); if (basicAuth != null) { @@ -84,6 +87,14 @@ public CompletableFuture produceRpcFuture(RpcCall call) { HttpRequest.Builder request = this.request.copy() .POST(HttpRequest.BodyPublishers.ofByteArray(rpcCoder.encode(id, call))); return httpClient.sendAsync(request.build(), HttpResponse.BodyHandlers.ofString()) + .exceptionallyCompose(ex -> { + if(ex instanceof CompletionException && ex.getCause() instanceof HttpTimeoutException) { + return CompletableFuture.failedFuture(new InterruptedIOException()); + } + else { + return CompletableFuture.failedFuture(ex); + } + }) .thenApply(this::verify) .thenApply(HttpResponse::body) .thenApply(content -> rpcCoder.decode(id, content, type)); diff --git a/polkaj-api-okhttp/build.gradle b/polkaj-api-okhttp/build.gradle new file mode 100644 index 0000000..5e2c79c --- /dev/null +++ b/polkaj-api-okhttp/build.gradle @@ -0,0 +1,42 @@ +apply from: '../common_java_app.gradle' +buildscript { + + ext.kotlin_version = '1.5.30' + ext.kotlin_coroutine_version = '1.5.2' + repositories { + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +apply plugin: 'kotlin' + +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} + +compileJava { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + api project(":polkaj-json-types") + api project(":polkaj-api-base") + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:$kotlin_coroutine_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$kotlin_coroutine_version" + implementation 'com.squareup.okhttp3:okhttp:4.9.1' + + 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-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt new file mode 100644 index 0000000..ddbc238 --- /dev/null +++ b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt @@ -0,0 +1,152 @@ +package io.emeraldpay.polkaj.apiokhttp + +import com.fasterxml.jackson.core.JsonProcessingException +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.RpcException +import io.emeraldpay.polkaj.json.jackson.PolkadotModule +import kotlinx.coroutines.* +import kotlinx.coroutines.future.future +import okhttp3.* +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.IOException +import java.lang.IllegalStateException +import java.time.Duration +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionException +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +class OkHttpRpcAdapter( + private val target : HttpUrl, + private val basicAuth : String?, + private val client : OkHttpClient, + private val scope : CoroutineScope, + private val rpcCoder: RpcCoder, + private val onClose : () -> Unit +) : RpcCallAdapter { + + companion object{ + private const val APPLICATION_JSON = "application/json" + } + + private var closed = false + private val baseRequest : Request = Request.Builder().apply { + url(target) + header("User-Agent", "PolkaJ/OkHttp/0.5") + header("Content-Type", APPLICATION_JSON) + if(basicAuth != null) header("Authorization", basicAuth) + }.build() + + override fun close() { + if(closed) return + closed = true + try{ + onClose() + }catch (t : Throwable){ + System.err.println("Error during onClose call: ${t.message}") + } + } + + override fun produceRpcFuture(call: RpcCall): CompletableFuture { + return if(closed){ + CompletableFuture.failedFuture(IllegalStateException("Client is already closed")) + } else{ + scope.future{ + await(call) + } + } + } + + suspend fun await(rpcCall : RpcCall): T { + return suspendCancellableCoroutine { continuation -> + val id = rpcCoder.nextId() + val type = rpcCall.getResultType(rpcCoder.objectMapper.typeFactory) + val call = baseRequest.newBuilder().post( + rpcCoder.encode(id, rpcCall).toRequestBody(APPLICATION_JSON.toMediaType()) + ).build().let { + client.newCall(it) + } + continuation.invokeOnCancellation { + call.cancel() + } + call.enqueue(object : Callback{ + override fun onFailure(call: Call, e: IOException) { + continuation.resumeWithException(e) + } + + override fun onResponse(call: Call, response: Response) { + if(response.code != 200){ + continuation.resumeWithException(RpcException( + -32000, "Server returned error status: ${response.code}" + )) + } else if(response.header("content-type", APPLICATION_JSON)?.startsWith(APPLICATION_JSON) == false){ + continuation.resumeWithException(RpcException( + -32000, "Server returned invalid content-type ${response.header("content-type")}" + )) + } else{ + try{ + rpcCoder.decode(id, response.body!!.byteStream(), type).let { + continuation.resume(it) + } + }catch (e : Throwable){ + when(e){ + is JsonProcessingException -> continuation.resumeWithException(RpcException(-32600, "Unable to encode request as JSON: ${e.message}")) + is CompletionException -> continuation.resumeWithException(e.cause ?: e) + else -> continuation.resumeWithException(e) + } + + } + } + } + }) + + } + } + + data class Builder( + private var target : HttpUrl = "http://127.0.0.1:9933".toHttpUrl(), + private var basicAuth : String? = null, + private var client : OkHttpClient = OkHttpClient.Builder().apply { + callTimeout(Duration.ofMinutes(1)) + followRedirects(false) + }.build(), + private var scope : CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO), + private var rpcCoder: RpcCoder = RpcCoder(ObjectMapper().registerModule(PolkadotModule())), + private var onClose : () -> Unit = { + scope.cancel() + client.dispatcher.executorService.shutdown() + } + ){ + companion object{ + inline operator fun invoke(block : Builder.() -> Builder) : OkHttpRpcAdapter { + return Builder().apply { block() }.build() + } + + } + + fun target(target: String) = apply { this.target = target.toHttpUrl() } + + fun basicAuth(username: String, password: String) : Builder{ + return apply { + val combine = "$username:$password".toByteArray() + basicAuth = "Basic ${Base64.getEncoder().encodeToString(combine)}" + } + } + + fun client(client : OkHttpClient) = apply { this.client = client } + fun scope(scope : CoroutineScope) = apply { this.scope = scope } + fun rpcCoder(rpcCoder : RpcCoder) = apply { this.rpcCoder = rpcCoder } + fun onClose(block : () -> Unit ) = apply { onClose = block } + fun timeout(timeout : Duration) = apply { client = client.newBuilder().callTimeout(timeout).build() } + fun build() : OkHttpRpcAdapter = OkHttpRpcAdapter(target, basicAuth, client, scope, rpcCoder, onClose) + } + +} \ No newline at end of file diff --git a/polkaj-api-okhttp/src/test/groovy/OkHttpRpcAdapterSpec.groovy b/polkaj-api-okhttp/src/test/groovy/OkHttpRpcAdapterSpec.groovy new file mode 100644 index 0000000..a767607 --- /dev/null +++ b/polkaj-api-okhttp/src/test/groovy/OkHttpRpcAdapterSpec.groovy @@ -0,0 +1,17 @@ +import io.emeraldpay.polkaj.api.RpcAdapterSpec +import io.emeraldpay.polkaj.api.RpcCallAdapter +import io.emeraldpay.polkaj.apiokhttp.OkHttpRpcAdapter + +import java.time.Duration + +class OkHttpRpcAdapterSpec extends RpcAdapterSpec { + + @Override + RpcCallAdapter provideAdapter(String connectTo, String username, String password, Duration timeout) { + return OkHttpRpcAdapter.Builder.@Companion.invoke({ builder -> + builder.target(connectTo) + .timeout(timeout) + .basicAuth(username, password) + }) + } +} diff --git a/settings.gradle b/settings.gradle index 299dcaf..ffe1f22 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,6 +11,7 @@ include "polkaj-ss58", "polkaj-api-base", "polkaj-api-http", "polkaj-api-ws", + "polkaj-api-okhttp", "polkaj-tx", "polkaj-adapter-tests" From 1a614207e48df20dba37cd698bc1583d63420c94 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sun, 12 Sep 2021 20:12:22 -0500 Subject: [PATCH 08/38] fix test --- .../java/io/emeraldpay/polkaj/api/StandardSubscriptions.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/StandardSubscriptions.java b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/StandardSubscriptions.java index a565448..a3ef78f 100644 --- a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/StandardSubscriptions.java +++ b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/StandardSubscriptions.java @@ -5,6 +5,7 @@ import io.emeraldpay.polkaj.json.StorageChangeSetJson; import io.emeraldpay.polkaj.types.ByteData; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -57,7 +58,9 @@ public SubscribeCall storage(List keys) { if (keys.isEmpty()) { return storage(); } - return SubscribeCall.create(StorageChangeSetJson.class, PolkadotMethod.STATE_SUBSCRIBE_STORAGE, PolkadotMethod.STATE_UNSUBSCRIBE_STORAGE, Collections.unmodifiableList(keys)); + final List> wrapper = new ArrayList<>(); + wrapper.add(keys); + return SubscribeCall.create(StorageChangeSetJson.class, PolkadotMethod.STATE_SUBSCRIBE_STORAGE, PolkadotMethod.STATE_UNSUBSCRIBE_STORAGE, Collections.unmodifiableList(wrapper)); } public SubscribeCall storage(ByteData key) { From 7301ace874e63861573f1738745a27ba065ee29b Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Tue, 14 Sep 2021 21:21:07 -0500 Subject: [PATCH 09/38] fix parameter list and improve test coverage --- .../io/emeraldpay/polkaj/api/StandardCommands.java | 4 +++- .../emeraldpay/polkaj/api/StandardCommandsSpec.groovy | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/StandardCommands.java b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/StandardCommands.java index b9c3ff1..696d82e 100644 --- a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/StandardCommands.java +++ b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/StandardCommands.java @@ -128,7 +128,9 @@ public RpcCall stateGetStorage(ByteData key) { } public RpcCall stateGetReadProof(List keys, Hash256 at) { - List params = new ArrayList<>(Collections.unmodifiableList(keys)); + List> wrapper = new ArrayList<>(); + wrapper.add(keys); + List params = new ArrayList<>(Collections.unmodifiableList(wrapper)); if (at != null) { params.add(at); } diff --git a/polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api/StandardCommandsSpec.groovy b/polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api/StandardCommandsSpec.groovy index a91b093..a4836d4 100644 --- a/polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api/StandardCommandsSpec.groovy +++ b/polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api/StandardCommandsSpec.groovy @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.type.TypeFactory import io.emeraldpay.polkaj.json.ContractCallRequestJson import io.emeraldpay.polkaj.json.ContractExecResultJson import io.emeraldpay.polkaj.json.MethodsJson +import io.emeraldpay.polkaj.json.ReadProofJson import io.emeraldpay.polkaj.json.RuntimeDispatchInfoJson import io.emeraldpay.polkaj.json.RuntimeVersionJson import io.emeraldpay.polkaj.json.SystemHealthJson @@ -154,6 +155,15 @@ class StandardCommandsSpec extends Specification { act.getResultType(typeFactory).getRawClass() == ByteData.class } + def "State Get Read Proof"() { + when: + def act = StandardCommands.getInstance().stateGetReadProof([ByteData.from("0x00")], Hash256.empty()) + then: + act.method == "state_getReadProof" + act.params.toList() == [[ByteData.from("0x00")], Hash256.empty()] + act.getResultType(typeFactory).getRawClass() == ReadProofJson.class + } + def "Contracts Call"() { setup: def call = new ContractCallRequestJson() From c76208e755d0a849c0036a366ac5f41c41513e47 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Thu, 16 Sep 2021 22:55:54 -0500 Subject: [PATCH 10/38] add android platform tests --- .github/workflows/test.yaml | 23 ++- build.gradle | 13 ++ gradle.properties | 2 + polkaj-api-okhttp/build.gradle | 11 -- polkaj-schnorrkel-android/build.gradle | 27 ++- .../SchnorrkelNativeAndroidTests.kt | 159 ++++++++++++++++++ .../polkaj/schnorrkel/SchnorrkelNative.java | 22 ++- polkaj-schnorrkel/build.gradle | 3 - .../polkaj/schnorrkel/SchnorrkelSpec.groovy | 4 - settings.gradle | 1 - 10 files changed, 225 insertions(+), 40 deletions(-) create mode 100644 polkaj-schnorrkel-android/src/androidTest/java/io/emeralpay/polkaj/schnorrkel/SchnorrkelNativeAndroidTests.kt diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e9ae1b5..ab93f3f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -3,8 +3,8 @@ name: Tests on: # if pushed directly to the master push: - branches: - - master +# branches: +# - master # on a pull request pull_request: branches: @@ -62,6 +62,25 @@ jobs: wrapper-cache-enabled: false arguments: check + # Make sure it works on supported Android versions + android-platform-test: + runs-on: macos-latest + strategy: + matrix: + api-level: [24, 25, 26, 27, 28, 29, 30] + arch: [x86_64, arm64-v8a, armeabi-v7a, x86] + steps: + - name: checkout + uses: actions/checkout@v2 + + - name: run tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + arch: ${{ matrix.arch }} + profile: Nexus 6 + script: ./gradlew connectedCheck + # Formatter may behave differently on different locales, makes sure tests are not failing locale-test: name: Locale ${{ matrix.locale }} diff --git a/build.gradle b/build.gradle index 3429a52..ceab035 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,20 @@ buildscript { + ext.kotlin_version = '1.5.30' + ext.kotlin_coroutine_version = '1.5.2' + + repositories { + google() + maven { + url "https://plugins.gradle.org/m2/" + } + mavenCentral() + } dependencies { classpath 'com.netflix.nebula:gradle-aggregate-javadocs-plugin:3.0.1' + classpath "com.android.tools.build:gradle:4.2.2" + classpath "gradle.plugin.com.github.willir.rust:plugin:0.3.4" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/gradle.properties b/gradle.properties index 2dbe70a..fc2c089 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,4 @@ org.gradle.jvmargs=-Dfile.encoding=UTF-8 org.gradle.workers.max=3 + +android.useAndroidX = true \ No newline at end of file diff --git a/polkaj-api-okhttp/build.gradle b/polkaj-api-okhttp/build.gradle index 5e2c79c..b99b4cb 100644 --- a/polkaj-api-okhttp/build.gradle +++ b/polkaj-api-okhttp/build.gradle @@ -1,15 +1,4 @@ apply from: '../common_java_app.gradle' -buildscript { - - ext.kotlin_version = '1.5.30' - ext.kotlin_coroutine_version = '1.5.2' - repositories { - mavenCentral() - } - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} apply plugin: 'kotlin' diff --git a/polkaj-schnorrkel-android/build.gradle b/polkaj-schnorrkel-android/build.gradle index 87c7de0..1011928 100644 --- a/polkaj-schnorrkel-android/build.gradle +++ b/polkaj-schnorrkel-android/build.gradle @@ -1,20 +1,7 @@ import java.nio.file.Paths -buildscript { - repositories { - google() - maven { - url "https://plugins.gradle.org/m2/" - } - mavenCentral() - } - dependencies { - classpath "com.android.tools.build:gradle:4.2.2" - classpath "gradle.plugin.com.github.willir.rust:plugin:0.3.4" - } -} - apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' apply plugin: "com.github.willir.rust.cargo-ndk-android" apply plugin: 'maven-publish' apply plugin: 'com.jfrog.bintray' @@ -30,7 +17,7 @@ android { versionCode 1 versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -39,17 +26,25 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } - + sourceSets { main { java { srcDirs = ["../polkaj-schnorrkel-common/src/main/java"] } } + } } + dependencies { + androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" + androidTestImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:runner:1.4.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'commons-codec:commons-codec:20041127.091804' } repositories { diff --git a/polkaj-schnorrkel-android/src/androidTest/java/io/emeralpay/polkaj/schnorrkel/SchnorrkelNativeAndroidTests.kt b/polkaj-schnorrkel-android/src/androidTest/java/io/emeralpay/polkaj/schnorrkel/SchnorrkelNativeAndroidTests.kt new file mode 100644 index 0000000..8ca5a40 --- /dev/null +++ b/polkaj-schnorrkel-android/src/androidTest/java/io/emeralpay/polkaj/schnorrkel/SchnorrkelNativeAndroidTests.kt @@ -0,0 +1,159 @@ +package io.emeralpay.polkaj.schnorrkel + +import io.emeraldpay.polkaj.schnorrkel.Schnorrkel +import io.emeraldpay.polkaj.schnorrkel.SchnorrkelException +import io.emeraldpay.polkaj.schnorrkel.SchnorrkelNative +import org.apache.commons.codec.binary.Hex +import org.junit.Test +import java.math.BigInteger +import java.security.SecureRandom +import kotlin.test.* + +class SchnorrkelNativeAndroidTests { + + private fun ByteArray.encodeHex() : String = String(Hex.encodeHex(this)) + private fun String.decodeHex() : ByteArray = Hex.decodeHex(toCharArray()) + + private val schnorrkel = SchnorrkelNative() + private val key1 = Schnorrkel.KeyPair( + "46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a".decodeHex(), + "28b0ae221c6bb06856b287f60d7ea0d98552ea5a16db16956849aa371db3eb51fd190cce74df356432b410bd64682309d6dedb27c76845daf388557cbac3ca34".decodeHex() + ) + + @Test + fun canSign(){ + val result = schnorrkel.sign("".toByteArray(), key1) + assertNotNull(result) + assertEquals(64, result.size) + } + + @Test + fun throwsErrorOnShortSk(){ + assertFailsWith("SecretKey must be 64 bytes in length"){ + schnorrkel.sign("".toByteArray(), + Schnorrkel.KeyPair( + "46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a".decodeHex(), + "28b0".decodeHex() + ) + ) + } + } + + @Test + fun signatureIsValid(){ + val msg = "hello".toByteArray() + val sig = schnorrkel.sign(msg, key1) + assertTrue { + schnorrkel.verify(sig, msg, key1) + } + } + + @Test + fun modifiedSignatureIsInvalid(){ + val msg = "hello".toByteArray() + val sig = schnorrkel.sign(msg, key1) + assertTrue { + schnorrkel.verify(sig, msg, key1) + } + assertFalse { + sig[0] = (sig[0] + 1).toByte() + schnorrkel.verify(sig, msg, key1) + } + } + + @Test + fun differentSignatureIsInvalid(){ + val msg = "hello".toByteArray() + val sig1 = schnorrkel.sign(msg, key1) + val sig2 = schnorrkel.sign("hello2".toByteArray(), key1) + assertTrue { + schnorrkel.verify(sig1, msg, key1) + } + assertFalse { + schnorrkel.verify(sig2, msg, key1) + } + } + + @Test + fun throwsErrorOnInvalidSignature(){ + val msg = "hello".toByteArray() + assertFailsWith { + schnorrkel.verify("00112233".decodeHex(), msg, key1) + } + } + + @Test + fun throwsErrorOnInvalidPubkey(){ + val msg = "hello".toByteArray() + val sig = schnorrkel.sign(msg, key1) + assertFailsWith { + schnorrkel.verify(sig, msg, Schnorrkel.PublicKey("11223344".decodeHex())) + } + } + + @Test + fun generatesWorkingKey(){ + val random = SecureRandom() + val msg = "hello".toByteArray() + val keypair = schnorrkel.generateKeyPair(random) + + assertNotNull(keypair) + assertEquals(Schnorrkel.PUBLIC_KEY_LENGTH, keypair.publicKey.size) + assertEquals(Schnorrkel.SECRET_KEY_LENGTH, keypair.secretKey.size) + assertNotEquals(BigInteger.ZERO, BigInteger(1, keypair.publicKey)) + assertNotEquals(BigInteger.ZERO, BigInteger(1, keypair.secretKey)) + + val sig = schnorrkel.sign(msg, keypair) + assertTrue { + schnorrkel.verify(sig, msg, keypair) + } + } + + @Test + fun generatesKeyFromDefaultSecureRandom(){ + val keypair = schnorrkel.generateKeyPair() + assertNotNull(keypair) + assertEquals(Schnorrkel.PUBLIC_KEY_LENGTH, keypair.publicKey.size) + assertEquals(Schnorrkel.SECRET_KEY_LENGTH, keypair.secretKey.size) + assertNotEquals(BigInteger.ZERO, BigInteger(1, keypair.publicKey)) + assertNotEquals(BigInteger.ZERO, BigInteger(1, keypair.secretKey)) + } + + @Test + fun generatesFromSeed(){ + val keypair = schnorrkel.generateKeyPairFromSeed("fac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e".decodeHex()) + assertNotNull(keypair) + assertEquals(Schnorrkel.PUBLIC_KEY_LENGTH, keypair.publicKey.size) + assertEquals(Schnorrkel.SECRET_KEY_LENGTH, keypair.secretKey.size) + assertNotEquals(BigInteger.ZERO, BigInteger(1, keypair.publicKey)) + assertNotEquals(BigInteger.ZERO, BigInteger(1, keypair.secretKey)) + assertEquals("46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a", keypair.publicKey.encodeHex()) + } + + @Test + fun deriveKey(){ + val seed = "fac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e".decodeHex() + val cc = Schnorrkel.ChainCode.from("14416c696365".decodeHex()) // Alice + val base = schnorrkel.generateKeyPairFromSeed(seed) + val keypair = schnorrkel.deriveKeyPair(base, cc) + assertEquals("d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", keypair.publicKey.encodeHex()) + } + + @Test + fun deriveKeySoft(){ + val seed = "fac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e".decodeHex() + val cc = Schnorrkel.ChainCode("0c666f6f00000000000000000000000000000000000000000000000000000000".decodeHex()) + val base = schnorrkel.generateKeyPairFromSeed(seed) + val keypair = schnorrkel.deriveKeyPairSoft(base, cc) + assertEquals("40b9675df90efa6069ff623b0fdfcf706cd47ca7452a5056c7ad58194d23440a", keypair.publicKey.encodeHex()) + } + + @Test + fun deriveSoftPublicKey(){ + val pub = "46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a".decodeHex() + val cc = Schnorrkel.ChainCode.from("0c666f6f00000000000000000000000000000000000000000000000000000000".decodeHex()) + val softKey = schnorrkel.derivePublicKeySoft(Schnorrkel.PublicKey(pub), cc) + assertEquals("40b9675df90efa6069ff623b0fdfcf706cd47ca7452a5056c7ad58194d23440a", softKey.publicKey.encodeHex()) + } + +} \ No newline at end of file diff --git a/polkaj-schnorrkel-common/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java b/polkaj-schnorrkel-common/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java index 62bc63a..5b7b29b 100644 --- a/polkaj-schnorrkel-common/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java +++ b/polkaj-schnorrkel-common/src/main/java/io/emeraldpay/polkaj/schnorrkel/SchnorrkelNative.java @@ -30,11 +30,19 @@ public boolean verify(byte[] signature, byte[] message, PublicKey publicKey) thr @Override public KeyPair generateKeyPair() throws SchnorrkelException { - try { - return generateKeyPair(SecureRandom.getInstanceStrong()); - } catch (NoSuchAlgorithmException e) { + SecureRandom secureRandom; + try{ + try{ + SecureRandom.class.getMethod("getInstanceStrong"); + //noinspection NewApi + secureRandom = SecureRandom.getInstanceStrong(); + }catch (NoSuchMethodException e){ + secureRandom = new SecureRandom(); //Android 24 & 25 do not have getInstanceStrong + } + }catch(NoSuchAlgorithmException e){ throw new SchnorrkelException("Secure Random is not available"); } + return generateKeyPair(secureRandom); } @Override @@ -117,6 +125,8 @@ private static byte[] encodeKeyPair(Schnorrkel.KeyPair keyPair) { } } + ////noinspection NewApi added to allow android lint check to succeed for api 24-25 + // these apis will not be called in that case private static boolean extractAndLoadJNI() throws IOException { // define which of files bundled with Jar to extract if(System.getProperty("java.runtime.name", "unknown").contains("android")) return false; @@ -141,15 +151,21 @@ private static boolean extractAndLoadJNI() throws IOException { System.err.println("Library " + classpathFile + " is not found in the classpath"); return false; } + //noinspection NewApi Path dir = Files.createTempDirectory(LIBNAME); + //noinspection NewApi Path target = dir.resolve(filename); + //noinspection NewApi Files.copy(lib, target); + //noinspection NewApi System.load(target.toFile().getAbsolutePath()); System.out.println("library " + classpathFile + " is loaded"); // setup JVM to delete files on exit, when possible + //noinspection NewApi target.toFile().deleteOnExit(); + //noinspection NewApi dir.toFile().deleteOnExit(); return true; } diff --git a/polkaj-schnorrkel/build.gradle b/polkaj-schnorrkel/build.gradle index 1b78943..f985c56 100644 --- a/polkaj-schnorrkel/build.gradle +++ b/polkaj-schnorrkel/build.gradle @@ -1,8 +1,5 @@ apply from: '../common_java_app.gradle' -dependencies { -} - sourceSets { main { java { diff --git a/polkaj-schnorrkel/src/test/groovy/io/emeraldpay/polkaj/schnorrkel/SchnorrkelSpec.groovy b/polkaj-schnorrkel/src/test/groovy/io/emeraldpay/polkaj/schnorrkel/SchnorrkelSpec.groovy index d532a2b..b668596 100644 --- a/polkaj-schnorrkel/src/test/groovy/io/emeraldpay/polkaj/schnorrkel/SchnorrkelSpec.groovy +++ b/polkaj-schnorrkel/src/test/groovy/io/emeraldpay/polkaj/schnorrkel/SchnorrkelSpec.groovy @@ -2,12 +2,8 @@ package io.emeraldpay.polkaj.schnorrkel import nl.jqno.equalsverifier.EqualsVerifier import nl.jqno.equalsverifier.Warning -import org.apache.commons.codec.binary.Hex import spock.lang.Specification -import java.security.PrivateKey -import java.security.SecureRandom - class SchnorrkelSpec extends Specification { def "Provides native implementation"() { diff --git a/settings.gradle b/settings.gradle index ffe1f22..5ca2727 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,7 +5,6 @@ include "polkaj-ss58", "polkaj-json-types", "polkaj-scale-types", "polkaj-schnorrkel", - "polkaj-schnorrkel-common", "polkaj-schnorrkel-android", "polkaj-common-types", "polkaj-api-base", From 013c8cb9b677f1fe9cfa1ef0ca7fb35001b7f312 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Thu, 16 Sep 2021 23:33:10 -0500 Subject: [PATCH 11/38] use v2 java setup --- .github/workflows/test.yaml | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ab93f3f..8a216ed 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,19 +19,16 @@ jobs: - uses: actions/checkout@v2 - name: Set up JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v2 with: + distribution: 'adopt' java-version: 11 - name: Check - uses: eskatos/gradle-command-action@v1 - with: - arguments: check + run: ./gradlew check - name: Build Coverage Report - uses: eskatos/gradle-command-action@v1 - with: - arguments: coverageReport + run: ./gradlew coverageReport - name: Upload Coverage Report uses: codecov/codecov-action@v1 @@ -51,8 +48,9 @@ jobs: - uses: actions/checkout@v2 - name: Set up JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v2 with: + distribution: 'adopt' java-version: ${{ matrix.java }} - name: Check @@ -63,12 +61,13 @@ jobs: arguments: check # Make sure it works on supported Android versions + # Note: this action doesn't support running armv7 tests android-platform-test: runs-on: macos-latest strategy: matrix: api-level: [24, 25, 26, 27, 28, 29, 30] - arch: [x86_64, arm64-v8a, armeabi-v7a, x86] + arch: [x86_64, arm64-v8a, x86] steps: - name: checkout uses: actions/checkout@v2 @@ -98,13 +97,12 @@ jobs: sudo update-locale LANG=${{ matrix.locale }} - name: Set up JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v2 with: + distribution: 'adopt' java-version: 11 - name: Check - uses: eskatos/gradle-command-action@v1 - with: - arguments: check + run: ./gradlew check env: LANG: ${{ matrix.locale }} \ No newline at end of file From dd8fa02b22f1d7bbac8c8cbcb6a6762ffeeb4b1a Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Fri, 17 Sep 2021 02:04:21 -0500 Subject: [PATCH 12/38] remove java 12 api --- polkaj-api-http/build.gradle | 6 ++++ .../polkaj/apihttp/JavaHttpAdapter.java | 32 +++++++++---------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/polkaj-api-http/build.gradle b/polkaj-api-http/build.gradle index 64c53ec..d7e1bf6 100644 --- a/polkaj-api-http/build.gradle +++ b/polkaj-api-http/build.gradle @@ -5,9 +5,15 @@ compileJava { sourceCompatibility = '11' } +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + dependencies { api project(":polkaj-json-types") api project(":polkaj-api-base") + implementation 'com.spotify:completable-futures:0.3.5' testImplementation 'org.mock-server:mockserver-netty:5.10' testImplementation project(":polkaj-adapter-tests") diff --git a/polkaj-api-http/src/main/java/io/emeraldpay/polkaj/apihttp/JavaHttpAdapter.java b/polkaj-api-http/src/main/java/io/emeraldpay/polkaj/apihttp/JavaHttpAdapter.java index 9c931d3..e9f23b8 100644 --- a/polkaj-api-http/src/main/java/io/emeraldpay/polkaj/apihttp/JavaHttpAdapter.java +++ b/polkaj-api-http/src/main/java/io/emeraldpay/polkaj/apihttp/JavaHttpAdapter.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; +import com.spotify.futures.CompletableFutures; import io.emeraldpay.polkaj.api.*; import io.emeraldpay.polkaj.json.jackson.PolkadotModule; @@ -15,10 +16,7 @@ import java.net.http.HttpTimeoutException; import java.time.Duration; import java.util.Base64; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.*; import java.util.function.Function; /** @@ -86,18 +84,20 @@ public CompletableFuture produceRpcFuture(RpcCall call) { try { HttpRequest.Builder request = this.request.copy() .POST(HttpRequest.BodyPublishers.ofByteArray(rpcCoder.encode(id, call))); - return httpClient.sendAsync(request.build(), HttpResponse.BodyHandlers.ofString()) - .exceptionallyCompose(ex -> { - if(ex instanceof CompletionException && ex.getCause() instanceof HttpTimeoutException) { - return CompletableFuture.failedFuture(new InterruptedIOException()); - } - else { - return CompletableFuture.failedFuture(ex); - } - }) - .thenApply(this::verify) - .thenApply(HttpResponse::body) - .thenApply(content -> rpcCoder.decode(id, content, type)); + + final CompletableFuture> sendAsync = httpClient.sendAsync(request.build(), + HttpResponse.BodyHandlers.ofString()); + return CompletableFutures.exceptionallyCompose(sendAsync, ex -> { + if(ex instanceof CompletionException && ex.getCause() instanceof HttpTimeoutException) { + return CompletableFuture.failedFuture(new InterruptedIOException()); + } + else { + return CompletableFuture.failedFuture(ex); + } + }).toCompletableFuture() + .thenApply(this::verify) + .thenApply(HttpResponse::body) + .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) From 8656daa9fcf3eac28e7933dce99efc63aff2b717 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Fri, 17 Sep 2021 02:04:51 -0500 Subject: [PATCH 13/38] add java 8 tests --- .github/workflows/test.yaml | 30 +++++++++++++++++++++++--- build.gradle | 2 ++ common_java_app.gradle | 9 ++++++++ polkaj-adapter-tests/build.gradle | 12 +++++++++-- polkaj-api-okhttp/build.gradle | 5 +++++ polkaj-api-ws/build.gradle | 4 ++++ polkaj-schnorrkel-android/build.gradle | 2 +- 7 files changed, 58 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8a216ed..e0358c6 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -25,16 +25,38 @@ jobs: java-version: 11 - name: Check - run: ./gradlew check + uses: eskatos/gradle-command-action@v1 + with: + arguments: check - name: Build Coverage Report - run: ./gradlew coverageReport + uses: eskatos/gradle-command-action@v1 + with: + arguments: coverageReport - name: Upload Coverage Report uses: codecov/codecov-action@v1 with: file: ./build/reports/jacoco/coverageReport/coverageReport.xml + # Run tests for java 8 targets + java8-test: + name: Java 8 Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up JDK + uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: 8 + + - name: Check + uses: eskatos/gradle-command-action@v1 + with: + arguments: checkJava8 + # Make sure it works with all standard JVMs on main OSes platform-test: name: Java ${{ matrix.java }} on ${{ matrix.os }} @@ -103,6 +125,8 @@ jobs: java-version: 11 - name: Check - run: ./gradlew check + uses: eskatos/gradle-command-action@v1 + with: + arguments: check env: LANG: ${{ matrix.locale }} \ No newline at end of file diff --git a/build.gradle b/build.gradle index ceab035..b51eb99 100644 --- a/build.gradle +++ b/build.gradle @@ -61,6 +61,8 @@ task syncJars(type: Sync) { into "${buildDir}/libs/" } +task checkJava8 + // Skip Bintray for the root module bintray { dryRun=true diff --git a/common_java_app.gradle b/common_java_app.gradle index de4b88b..2045d88 100644 --- a/common_java_app.gradle +++ b/common_java_app.gradle @@ -9,12 +9,21 @@ 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) + + if(java.sourceCompatibility == JavaVersion.VERSION_1_8){ + rootProject.tasks.checkJava8.dependsOn(test) + } } tasks.withType(JavaCompile) { options.debug = true } +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + compileJava { targetCompatibility = '8' sourceCompatibility = '8' diff --git a/polkaj-adapter-tests/build.gradle b/polkaj-adapter-tests/build.gradle index 54b154d..a6441aa 100644 --- a/polkaj-adapter-tests/build.gradle +++ b/polkaj-adapter-tests/build.gradle @@ -1,8 +1,16 @@ -//apply from: '../common_java_app.gradle' - apply plugin: 'java-library' apply plugin: 'groovy' +compileJava { + targetCompatibility = '11' + sourceCompatibility = '11' +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + dependencies { api project(":polkaj-json-types") api project(":polkaj-api-base") diff --git a/polkaj-api-okhttp/build.gradle b/polkaj-api-okhttp/build.gradle index b99b4cb..09fb4ea 100644 --- a/polkaj-api-okhttp/build.gradle +++ b/polkaj-api-okhttp/build.gradle @@ -18,6 +18,11 @@ compileJava { targetCompatibility = JavaVersion.VERSION_1_8 } +compileTestJava { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + dependencies { api project(":polkaj-json-types") api project(":polkaj-api-base") diff --git a/polkaj-api-ws/build.gradle b/polkaj-api-ws/build.gradle index 496ee64..afdaea3 100644 --- a/polkaj-api-ws/build.gradle +++ b/polkaj-api-ws/build.gradle @@ -5,6 +5,10 @@ compileJava { sourceCompatibility = '11' } +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} dependencies { api project(":polkaj-json-types") diff --git a/polkaj-schnorrkel-android/build.gradle b/polkaj-schnorrkel-android/build.gradle index 1011928..5ba066e 100644 --- a/polkaj-schnorrkel-android/build.gradle +++ b/polkaj-schnorrkel-android/build.gradle @@ -26,7 +26,7 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } - + sourceSets { main { java { From 68700519fd3c7627c8975f648bfcca0f744eedac Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Fri, 17 Sep 2021 02:34:17 -0500 Subject: [PATCH 14/38] resolve github action errors --- .github/workflows/test.yaml | 7 +++++-- polkaj-adapter-tests/build.gradle | 8 ++++---- .../groovy/io/emeraldpay/polkaj/api/RpcAdapterSpec.groovy | 1 - polkaj-api-okhttp/build.gradle | 5 +---- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e0358c6..7101a11 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -41,8 +41,11 @@ jobs: # Run tests for java 8 targets java8-test: - name: Java 8 Test - runs-on: ubuntu-latest + name: Java 8 Test on ${{ matrix.os }} + strategy: + matrix: + os: [ "windows-latest", "macos-latest", "ubuntu-latest" ] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 diff --git a/polkaj-adapter-tests/build.gradle b/polkaj-adapter-tests/build.gradle index a6441aa..8c2f913 100644 --- a/polkaj-adapter-tests/build.gradle +++ b/polkaj-adapter-tests/build.gradle @@ -2,13 +2,13 @@ apply plugin: 'java-library' apply plugin: 'groovy' compileJava { - targetCompatibility = '11' - sourceCompatibility = '11' + targetCompatibility = '8' + sourceCompatibility = '8' } java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } dependencies { diff --git a/polkaj-adapter-tests/src/main/groovy/io/emeraldpay/polkaj/api/RpcAdapterSpec.groovy b/polkaj-adapter-tests/src/main/groovy/io/emeraldpay/polkaj/api/RpcAdapterSpec.groovy index 3b99add..eca05c0 100644 --- a/polkaj-adapter-tests/src/main/groovy/io/emeraldpay/polkaj/api/RpcAdapterSpec.groovy +++ b/polkaj-adapter-tests/src/main/groovy/io/emeraldpay/polkaj/api/RpcAdapterSpec.groovy @@ -10,7 +10,6 @@ import org.mockserver.model.MediaType import spock.lang.Shared import spock.lang.Specification -import java.net.http.HttpTimeoutException import java.nio.charset.Charset import java.time.Duration import java.util.concurrent.ExecutionException diff --git a/polkaj-api-okhttp/build.gradle b/polkaj-api-okhttp/build.gradle index 09fb4ea..0ccdf56 100644 --- a/polkaj-api-okhttp/build.gradle +++ b/polkaj-api-okhttp/build.gradle @@ -18,10 +18,7 @@ compileJava { targetCompatibility = JavaVersion.VERSION_1_8 } -compileTestJava { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 -} + dependencies { api project(":polkaj-json-types") From 31ddfcc3f81abdca44779531b8b87bdf9e5ada1e Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Fri, 17 Sep 2021 16:54:52 -0500 Subject: [PATCH 15/38] remove java 9 api --- .../kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt index ddbc238..658d6e3 100644 --- a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt +++ b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt @@ -57,7 +57,9 @@ class OkHttpRpcAdapter( override fun produceRpcFuture(call: RpcCall): CompletableFuture { return if(closed){ - CompletableFuture.failedFuture(IllegalStateException("Client is already closed")) + CompletableFuture().apply { + completeExceptionally(IllegalStateException("Client is already closed")) + } } else{ scope.future{ await(call) From c4d276d80f3a057b155d2b61ad8fda6e086f4c66 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sat, 18 Sep 2021 18:50:28 -0500 Subject: [PATCH 16/38] fix CI issues --- .github/workflows/publish.yaml | 2 ++ .github/workflows/test.yaml | 20 ++++++++++++++----- build.gradle | 2 +- cargo_ndk_prep.sh | 3 +++ .../polkaj/apiokhttp/OkHttpRpcAdapter.kt | 10 ++++++++++ 5 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 cargo_ndk_prep.sh diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index c54f834..c5e6f01 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -62,6 +62,8 @@ jobs: with: name: schnorrkel-lib path: polkaj-schnorrkel/build/rust/release + - name: Setup Rust for Android + run: ./cargo_ndk_prep.sh - name: Check uses: eskatos/gradle-command-action@v1 with: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7101a11..e1e4f44 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -41,14 +41,15 @@ jobs: # Run tests for java 8 targets java8-test: - name: Java 8 Test on ${{ matrix.os }} - strategy: - matrix: - os: [ "windows-latest", "macos-latest", "ubuntu-latest" ] - runs-on: ${{ matrix.os }} + name: Java 8 Test + runs-on: "ubuntu-latest" steps: + - name: Checkout - uses: actions/checkout@v2 + - name: Setup Rust + run: ./cargo_ndk_prep.sh + - name: Set up JDK uses: actions/setup-java@v2 with: @@ -72,6 +73,9 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Setup Rust + run: ./cargo_ndk_prep.sh + - name: Set up JDK uses: actions/setup-java@v2 with: @@ -97,6 +101,9 @@ jobs: - name: checkout uses: actions/checkout@v2 + - name: Setup Rust + run: ./cargo_ndk_prep.sh + - name: run tests uses: reactivecircus/android-emulator-runner@v2 with: @@ -116,6 +123,9 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Setup Rust + run: ./cargo_ndk_prep.sh + - name: Set Locale run: | sudo locale-gen ${{ matrix.locale }} diff --git a/build.gradle b/build.gradle index b51eb99..b340479 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ allprojects { } jacoco { - toolVersion = "0.8.5" + toolVersion = "0.8.7" } task coverageReport(type: JacocoReport) { diff --git a/cargo_ndk_prep.sh b/cargo_ndk_prep.sh new file mode 100644 index 0000000..f3fabe6 --- /dev/null +++ b/cargo_ndk_prep.sh @@ -0,0 +1,3 @@ +#!/bin/sh +rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android +cargo install cargo-ndk \ No newline at end of file diff --git a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt index 658d6e3..f4dc1f3 100644 --- a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt +++ b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt @@ -67,6 +67,16 @@ class OkHttpRpcAdapter( } } + fun getCall(rpcCall: RpcCall) : Call{ + val id = rpcCoder.nextId() + val type = rpcCall.getResultType(rpcCoder.objectMapper.typeFactory) + return baseRequest.newBuilder().post( + rpcCoder.encode(id, rpcCall).toRequestBody(APPLICATION_JSON.toMediaType()) + ).build().let { + client.newCall(it) + } + } + suspend fun await(rpcCall : RpcCall): T { return suspendCancellableCoroutine { continuation -> val id = rpcCoder.nextId() From 315c66a664a3206c79a8d6288f678d379481c1f9 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sat, 18 Sep 2021 19:25:28 -0500 Subject: [PATCH 17/38] more ci fixes --- .github/workflows/test.yaml | 1 - cargo_ndk_prep.sh | 0 2 files changed, 1 deletion(-) mode change 100644 => 100755 cargo_ndk_prep.sh diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e1e4f44..ded5d4e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -44,7 +44,6 @@ jobs: name: Java 8 Test runs-on: "ubuntu-latest" steps: - - name: Checkout - uses: actions/checkout@v2 - name: Setup Rust diff --git a/cargo_ndk_prep.sh b/cargo_ndk_prep.sh old mode 100644 new mode 100755 From b16f352d70ba5895ae892532a2a0b07b724c9bfb Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sat, 18 Sep 2021 20:53:17 -0500 Subject: [PATCH 18/38] add java 8 target to compile options --- common_java_app.gradle | 4 ++++ .../java/io/emeraldpay/polkaj/scale/reader/Int32Reader.java | 3 ++- .../main/java/io/emeraldpay/polkaj/tx/AccountRequests.java | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/common_java_app.gradle b/common_java_app.gradle index 2045d88..698c889 100644 --- a/common_java_app.gradle +++ b/common_java_app.gradle @@ -10,8 +10,12 @@ afterEvaluate { 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) + //By default we set to java 8 but some project override this to higher levels so after evaluate we if(java.sourceCompatibility == JavaVersion.VERSION_1_8){ rootProject.tasks.checkJava8.dependsOn(test) + tasks.withType(JavaCompile).all { + it.options.compilerArgs += ['--release', '8'] + } } } diff --git a/polkaj-scale/src/main/java/io/emeraldpay/polkaj/scale/reader/Int32Reader.java b/polkaj-scale/src/main/java/io/emeraldpay/polkaj/scale/reader/Int32Reader.java index 7388a26..32c72b6 100644 --- a/polkaj-scale/src/main/java/io/emeraldpay/polkaj/scale/reader/Int32Reader.java +++ b/polkaj-scale/src/main/java/io/emeraldpay/polkaj/scale/reader/Int32Reader.java @@ -21,6 +21,7 @@ public Integer read(ScaleCodecReader rdr) { buf.put(rdr.readByte()); buf.put(rdr.readByte()); buf.put(rdr.readByte()); - return buf.flip().getInt(); + //https://www.morling.dev/blog/bytebuffer-and-the-dreaded-nosuchmethoderror/ + return ((ByteBuffer)buf.flip()).getInt(); } } diff --git a/polkaj-tx/src/main/java/io/emeraldpay/polkaj/tx/AccountRequests.java b/polkaj-tx/src/main/java/io/emeraldpay/polkaj/tx/AccountRequests.java index 0ef93fb..8a4ee68 100644 --- a/polkaj-tx/src/main/java/io/emeraldpay/polkaj/tx/AccountRequests.java +++ b/polkaj-tx/src/main/java/io/emeraldpay/polkaj/tx/AccountRequests.java @@ -73,7 +73,7 @@ public ByteData encodeRequest() { ByteBuffer buffer = ByteBuffer.allocate(len); Hashing.xxhash128(buffer, key1); Hashing.xxhash128(buffer, key2); - return new ByteData(buffer.flip().array()); + return new ByteData(((ByteBuffer)buffer.flip()).array()); } @Override @@ -100,7 +100,7 @@ public ByteData encodeRequest() { Hashing.xxhash128(buffer, key2); Hashing.blake2128(buffer, address); buffer.put(address.getPubkey()); - return new ByteData(buffer.flip().array()); + return new ByteData(((ByteBuffer)buffer.flip()).array()); } @Override From f0350946358d22410d275711904130bc9907f568 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sat, 18 Sep 2021 20:53:28 -0500 Subject: [PATCH 19/38] more CI fixes --- .github/workflows/test.yaml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ded5d4e..ffbafc5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -46,9 +46,6 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Setup Rust - run: ./cargo_ndk_prep.sh - - name: Set up JDK uses: actions/setup-java@v2 with: @@ -72,9 +69,6 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Setup Rust - run: ./cargo_ndk_prep.sh - - name: Set up JDK uses: actions/setup-java@v2 with: @@ -88,14 +82,19 @@ jobs: wrapper-cache-enabled: false arguments: check - # Make sure it works on supported Android versions - # Note: this action doesn't support running armv7 tests + #Test with android emulators provided android-platform-test: runs-on: macos-latest strategy: matrix: api-level: [24, 25, 26, 27, 28, 29, 30] - arch: [x86_64, arm64-v8a, x86] + arch: [x86_64, x86] + include: + - api-level: 24 + arch: arm64-v8a + - api-level: 30 + arch: arm64-v8a + target: google_apis steps: - name: checkout uses: actions/checkout@v2 @@ -106,9 +105,9 @@ jobs: - name: run tests uses: reactivecircus/android-emulator-runner@v2 with: +# ndk: 21.4.7075529 ## This would install the NDK version, but it's already installed in the image. api-level: ${{ matrix.api-level }} arch: ${{ matrix.arch }} - profile: Nexus 6 script: ./gradlew connectedCheck # Formatter may behave differently on different locales, makes sure tests are not failing From 8de4767e036249996ef0be1a29eac888dc679650 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sat, 18 Sep 2021 21:30:48 -0500 Subject: [PATCH 20/38] remove release target and add local properties file to ci --- .github/workflows/test.yaml | 2 +- common_java_app.gradle | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ffbafc5..fe94e59 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -100,7 +100,7 @@ jobs: uses: actions/checkout@v2 - name: Setup Rust - run: ./cargo_ndk_prep.sh + run: ./cargo_ndk_prep.sh; touch local.properties - name: run tests uses: reactivecircus/android-emulator-runner@v2 diff --git a/common_java_app.gradle b/common_java_app.gradle index 698c889..b3b7217 100644 --- a/common_java_app.gradle +++ b/common_java_app.gradle @@ -13,9 +13,6 @@ afterEvaluate { //By default we set to java 8 but some project override this to higher levels so after evaluate we if(java.sourceCompatibility == JavaVersion.VERSION_1_8){ rootProject.tasks.checkJava8.dependsOn(test) - tasks.withType(JavaCompile).all { - it.options.compilerArgs += ['--release', '8'] - } } } From dc7af2d6ae9ba3b98393e78a3ffd65daed9aa974 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sat, 18 Sep 2021 21:58:11 -0500 Subject: [PATCH 21/38] remove java 9 api from test --- .../polkaj/scale/reader/BoolOptionalReaderSpec.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polkaj-scale/src/test/groovy/io/emeraldpay/polkaj/scale/reader/BoolOptionalReaderSpec.groovy b/polkaj-scale/src/test/groovy/io/emeraldpay/polkaj/scale/reader/BoolOptionalReaderSpec.groovy index 4628365..c2c278d 100644 --- a/polkaj-scale/src/test/groovy/io/emeraldpay/polkaj/scale/reader/BoolOptionalReaderSpec.groovy +++ b/polkaj-scale/src/test/groovy/io/emeraldpay/polkaj/scale/reader/BoolOptionalReaderSpec.groovy @@ -36,7 +36,7 @@ class BoolOptionalReaderSpec extends Specification { when: def act = codec.read(reader) then: - act.isEmpty() + !act.isPresent() !codec.hasNext() } From 4e0f4ac66ce831185da33f5a7e4ca00de2205d8d Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sat, 18 Sep 2021 22:36:36 -0500 Subject: [PATCH 22/38] remove api 24 arm test from github it can not connect to the emulator --- .github/workflows/test.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fe94e59..a21fa6a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -90,8 +90,6 @@ jobs: api-level: [24, 25, 26, 27, 28, 29, 30] arch: [x86_64, x86] include: - - api-level: 24 - arch: arm64-v8a - api-level: 30 arch: arm64-v8a target: google_apis From 30ec4b0771cf1655cb2c249d6f7f74ed98e188d3 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sat, 18 Sep 2021 23:08:22 -0500 Subject: [PATCH 23/38] remove unsupported test configuration --- .github/workflows/test.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a21fa6a..30f8bec 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -93,6 +93,9 @@ jobs: - api-level: 30 arch: arm64-v8a target: google_apis + exclude: + - api-level: 30 + arch: x86 steps: - name: checkout uses: actions/checkout@v2 From 762093b34a801ae3858c1f6de69901f718f8d85c Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sat, 18 Sep 2021 23:53:23 -0500 Subject: [PATCH 24/38] use google_apis instead of default target for api 30 testing default is missing. --- .github/workflows/test.yaml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 30f8bec..9d47f4b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -87,15 +87,18 @@ jobs: runs-on: macos-latest strategy: matrix: - api-level: [24, 25, 26, 27, 28, 29, 30] + api-level: [24, 25, 26, 27, 28, 29] arch: [x86_64, x86] include: - api-level: 30 arch: arm64-v8a target: google_apis - exclude: - - api-level: 30 - arch: x86 + - api-level: 30 + arch: x86 + target: google_apis + - api-level: 30 + arch: x86_64 + target: google_apis steps: - name: checkout uses: actions/checkout@v2 @@ -106,7 +109,7 @@ jobs: - name: run tests uses: reactivecircus/android-emulator-runner@v2 with: -# ndk: 21.4.7075529 ## This would install the NDK version, but it's already installed in the image. +# ndk: 21.4.7075529 ## This would install the correct NDK version, but it's already installed in the image. api-level: ${{ matrix.api-level }} arch: ${{ matrix.arch }} script: ./gradlew connectedCheck From d5f782521be965aa595669725ab22749464728a7 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sun, 19 Sep 2021 00:02:05 -0500 Subject: [PATCH 25/38] fix invalid action syntax --- .github/workflows/publish.yaml | 2 +- .github/workflows/test.yaml | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index c5e6f01..8b46d1c 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -63,7 +63,7 @@ jobs: name: schnorrkel-lib path: polkaj-schnorrkel/build/rust/release - name: Setup Rust for Android - run: ./cargo_ndk_prep.sh + run: ./cargo_ndk_prep.sh; touch local.properties - name: Check uses: eskatos/gradle-command-action@v1 with: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9d47f4b..df4b4cb 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -91,14 +91,9 @@ jobs: arch: [x86_64, x86] include: - api-level: 30 - arch: arm64-v8a + arch: [arm64-v8a, x86, x86_64] target: google_apis - - api-level: 30 - arch: x86 - target: google_apis - - api-level: 30 - arch: x86_64 - target: google_apis + steps: - name: checkout uses: actions/checkout@v2 From 575ac224ae9378ad463dce98076afbd49bc05eeb Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sun, 19 Sep 2021 13:38:10 -0500 Subject: [PATCH 26/38] update matrix config --- .github/workflows/test.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index df4b4cb..74c5ec9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -87,12 +87,15 @@ jobs: runs-on: macos-latest strategy: matrix: - api-level: [24, 25, 26, 27, 28, 29] + api-level: [24, 25, 26, 27, 28, 29, 30] arch: [x86_64, x86] include: - api-level: 30 - arch: [arm64-v8a, x86, x86_64] + arch: arm64-v8a target: google_apis + exclude: + - api-level: 30 + target: default steps: - name: checkout From fe61cae42879345ebd882e094c6c8fe12739f77c Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sun, 19 Sep 2021 13:56:33 -0500 Subject: [PATCH 27/38] remove failing exclude --- .github/workflows/test.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 74c5ec9..abeb35b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -93,9 +93,6 @@ jobs: - api-level: 30 arch: arm64-v8a target: google_apis - exclude: - - api-level: 30 - target: default steps: - name: checkout From ce6a00175ae154aa3b601d772b8492a3e5c7e824 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sun, 19 Sep 2021 14:00:14 -0500 Subject: [PATCH 28/38] grokking the matrix better --- .github/workflows/test.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index abeb35b..4be1f60 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -89,10 +89,16 @@ jobs: matrix: api-level: [24, 25, 26, 27, 28, 29, 30] arch: [x86_64, x86] + target: default include: - api-level: 30 arch: arm64-v8a target: google_apis + - api-level: 30 + target: google_apis + exclude: + - api-level: 30 + target: default steps: - name: checkout From 63963d62e684cc60263922e7127d11e895050b49 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sun, 19 Sep 2021 14:04:54 -0500 Subject: [PATCH 29/38] make target an array --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 4be1f60..caba547 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -89,7 +89,7 @@ jobs: matrix: api-level: [24, 25, 26, 27, 28, 29, 30] arch: [x86_64, x86] - target: default + target: [default] include: - api-level: 30 arch: arm64-v8a From 1cc6f451d256fbcce939e383328fd011820c5df3 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sun, 19 Sep 2021 14:07:43 -0500 Subject: [PATCH 30/38] correct missing arch --- .github/workflows/test.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index caba547..340cceb 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -95,6 +95,10 @@ jobs: arch: arm64-v8a target: google_apis - api-level: 30 + arch: x86_64 + target: google_apis + - api-level: 30 + arch: x86 target: google_apis exclude: - api-level: 30 From db9648bfd80256bd862a3bb990f56c93f229c2d0 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sun, 19 Sep 2021 14:50:27 -0500 Subject: [PATCH 31/38] make all android tests on google api targets --- .github/workflows/test.yaml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 340cceb..d43e4a3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -87,22 +87,13 @@ jobs: runs-on: macos-latest strategy: matrix: - api-level: [24, 25, 26, 27, 28, 29, 30] + api-level: [24, 25, 26, 28, 29, 30] arch: [x86_64, x86] - target: [default] + target: [google_apis] include: - api-level: 30 arch: arm64-v8a target: google_apis - - api-level: 30 - arch: x86_64 - target: google_apis - - api-level: 30 - arch: x86 - target: google_apis - exclude: - - api-level: 30 - target: default steps: - name: checkout From da1434a3a01fe461903b0d6fa7ce5432c5f0e55f Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sun, 19 Sep 2021 15:03:27 -0500 Subject: [PATCH 32/38] fix build script was still using default instead of google apis --- .github/workflows/test.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d43e4a3..74bc99d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -89,11 +89,9 @@ jobs: matrix: api-level: [24, 25, 26, 28, 29, 30] arch: [x86_64, x86] - target: [google_apis] include: - api-level: 30 arch: arm64-v8a - target: google_apis steps: - name: checkout @@ -108,6 +106,7 @@ jobs: # ndk: 21.4.7075529 ## This would install the correct NDK version, but it's already installed in the image. api-level: ${{ matrix.api-level }} arch: ${{ matrix.arch }} + target: google_apis script: ./gradlew connectedCheck # Formatter may behave differently on different locales, makes sure tests are not failing From f73e0dcef71dd82a29f8085afff8938362375fb7 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Tue, 21 Sep 2021 01:56:09 -0500 Subject: [PATCH 33/38] okhttp websockets --- .../polkaj/api/internal}/DecodeResponse.java | 12 +- .../api/internal/SubscriptionResponse.java | 42 +++ .../polkaj/api/internal}/WsResponse.java | 10 +- .../polkaj/api}/DecodeResponseSpec.groovy | 11 +- .../polkaj/api}/WsResponseSpec.groovy | 14 +- .../polkaj/apiokhttp/OkHttpRpcAdapter.kt | 103 +++--- .../apiokhttp/OkHttpSubscriptionAdapter.kt | 294 ++++++++++++++++++ .../OkHttpSubscriptionAdapterSpec.groovy | 16 + .../apiws/JavaHttpSubscriptionAdapter.java | 47 +-- 9 files changed, 433 insertions(+), 116 deletions(-) rename {polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws => polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/internal}/DecodeResponse.java (94%) create mode 100644 polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/internal/SubscriptionResponse.java rename {polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws => polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/internal}/WsResponse.java (85%) rename {polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws => polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api}/DecodeResponseSpec.groovy (96%) rename {polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws => polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api}/WsResponseSpec.groovy (57%) create mode 100644 polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt create mode 100644 polkaj-api-okhttp/src/test/groovy/OkHttpSubscriptionAdapterSpec.groovy diff --git a/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/DecodeResponse.java b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/internal/DecodeResponse.java similarity index 94% rename from polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/DecodeResponse.java rename to polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/internal/DecodeResponse.java index 79ee79d..7f47e42 100644 --- a/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/DecodeResponse.java +++ b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/internal/DecodeResponse.java @@ -1,4 +1,4 @@ -package io.emeraldpay.polkaj.apiws; +package io.emeraldpay.polkaj.api.internal; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; @@ -52,21 +52,21 @@ public WsResponse decode(final String json) throws IOException { preparsed.id = decodeNumber(parser); preparsed.type = findType(rpcMapping, preparsed.id); if (preparsed.isReady()) { - var result = preparsed.build(); + final WsResponse.IdValue result = preparsed.build(); return WsResponse.rpc(new RpcResponse<>(result.getId(), result.getValue())); } } else if ("result".equals(field)) { parser.nextToken(); preparsed.node = parser.readValueAsTree(); if (preparsed.isReady()) { - var result = preparsed.build(); + final WsResponse.IdValue result = preparsed.build(); return WsResponse.rpc(new RpcResponse<>(result.getId(), result.getValue())); } } else if ("error".equals(field)) { //TODO parse error preparsed.error = decodeError(parser); if (preparsed.id != null) { - var result = preparsed.build(); + final WsResponse.IdValue result = preparsed.build(); return WsResponse.rpc(new RpcResponse<>(result.getId(), result.getValue())); } } else if ("method".equals(field)) { @@ -74,14 +74,14 @@ public WsResponse decode(final String json) throws IOException { method = parser.getValueAsString(); if (value != null) { return WsResponse.subscription( - new JavaHttpSubscriptionAdapter.SubscriptionResponse<>(value.getId(), method, value.getValue()) + new SubscriptionResponse<>(value.getId(), method, value.getValue()) ); } } else if ("params".equals(field)) { value = decodeSubscription(subscriptionMapping, parser); if (method != null) { return WsResponse.subscription( - new JavaHttpSubscriptionAdapter.SubscriptionResponse<>(value.getId(), method, value.getValue()) + new SubscriptionResponse<>(value.getId(), method, value.getValue()) ); } } diff --git a/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/internal/SubscriptionResponse.java b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/internal/SubscriptionResponse.java new file mode 100644 index 0000000..ddea0c9 --- /dev/null +++ b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/internal/SubscriptionResponse.java @@ -0,0 +1,42 @@ +package io.emeraldpay.polkaj.api.internal; + +import java.util.Objects; + +public class SubscriptionResponse { + private final String id; + private final String method; + private final T value; + + public SubscriptionResponse(String id, String method, T value) { + this.id = id; + this.method = method; + this.value = value; + } + + public String getId() { + return id; + } + + public String getMethod() { + return method; + } + + public T getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SubscriptionResponse)) return false; + SubscriptionResponse that = (SubscriptionResponse) o; + return id.equals(that.id) && + Objects.equals(method, that.method) && + Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(id, method, value); + } +} diff --git a/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/WsResponse.java b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/internal/WsResponse.java similarity index 85% rename from polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/WsResponse.java rename to polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/internal/WsResponse.java index b40e181..00094e8 100644 --- a/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/WsResponse.java +++ b/polkaj-api-base/src/main/java/io/emeraldpay/polkaj/api/internal/WsResponse.java @@ -1,11 +1,11 @@ -package io.emeraldpay.polkaj.apiws; +package io.emeraldpay.polkaj.api.internal; import io.emeraldpay.polkaj.api.RpcResponse; /** * Container for the WebSocker message. A message may be a subscription event, or a response to a standard RPC call. * - * @see JavaHttpSubscriptionAdapter.SubscriptionResponse + * @see 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 JavaHttpSubscriptionAdapter.SubscriptionResponse asEvent() { + public SubscriptionResponse asEvent() { if (type != Type.SUBSCRIPTION) { throw new ClassCastException("Not an event"); } - return (JavaHttpSubscriptionAdapter.SubscriptionResponse) value; + return (SubscriptionResponse) value; } /** @@ -62,7 +62,7 @@ public RpcResponse asRpc() { * @param event event data * @return response instance configured for Subscription Event */ - public static WsResponse subscription(JavaHttpSubscriptionAdapter.SubscriptionResponse event) { + public static WsResponse subscription(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-base/src/test/groovy/io/emeraldpay/polkaj/api/DecodeResponseSpec.groovy similarity index 96% rename from polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/DecodeResponseSpec.groovy rename to polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api/DecodeResponseSpec.groovy index b13aa01..33fdded 100644 --- a/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/DecodeResponseSpec.groovy +++ b/polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api/DecodeResponseSpec.groovy @@ -1,6 +1,9 @@ -package io.emeraldpay.polkaj.apiws +package io.emeraldpay.polkaj.api import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.polkaj.api.internal.DecodeResponse +import io.emeraldpay.polkaj.api.internal.SubscriptionResponse +import io.emeraldpay.polkaj.api.internal.WsResponse import io.emeraldpay.polkaj.json.StorageChangeSetJson import io.emeraldpay.polkaj.types.ByteData import io.emeraldpay.polkaj.types.Hash256 @@ -151,7 +154,7 @@ class DecodeResponseSpec extends Specification { def act = decoder.decode(json) then: act.type == WsResponse.Type.SUBSCRIPTION - JavaHttpSubscriptionAdapter.SubscriptionResponse event = act.asEvent() + SubscriptionResponse event = act.asEvent() event.method == "chain_newHead" event.id == "EsqruyKPnZvPZ6fr" event.value instanceof BlockJson.Header @@ -187,7 +190,7 @@ class DecodeResponseSpec extends Specification { def act = decoder.decode(json) then: act.type == WsResponse.Type.SUBSCRIPTION - JavaHttpSubscriptionAdapter.SubscriptionResponse event = act.asEvent() + SubscriptionResponse event = act.asEvent() event.method == "chain_newHead" event.id == "EsqruyKPnZvPZ6fr" event.value instanceof BlockJson.Header @@ -224,7 +227,7 @@ class DecodeResponseSpec extends Specification { def act = decoder.decode(json) then: act.type == WsResponse.Type.SUBSCRIPTION - JavaHttpSubscriptionAdapter.SubscriptionResponse event = act.asEvent() + 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/WsResponseSpec.groovy b/polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api/WsResponseSpec.groovy similarity index 57% rename from polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/WsResponseSpec.groovy rename to polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api/WsResponseSpec.groovy index e020a20..e659825 100644 --- a/polkaj-api-ws/src/test/groovy/io/emeraldpay/polkaj/apiws/WsResponseSpec.groovy +++ b/polkaj-api-base/src/test/groovy/io/emeraldpay/polkaj/api/WsResponseSpec.groovy @@ -1,7 +1,7 @@ -package io.emeraldpay.polkaj.apiws +package io.emeraldpay.polkaj.api - -import io.emeraldpay.polkaj.api.RpcResponse +import io.emeraldpay.polkaj.api.internal.SubscriptionResponse +import io.emeraldpay.polkaj.api.internal.WsResponse import spock.lang.Specification class WsResponseSpec extends Specification { @@ -17,11 +17,11 @@ class WsResponseSpec extends Specification { def "Creates subscription response"() { when: - def act = WsResponse.subscription(new JavaHttpSubscriptionAdapter.SubscriptionResponse("EsqruyKPnZvPZ6fr", "test", "test")) + def act = WsResponse.subscription(new SubscriptionResponse("EsqruyKPnZvPZ6fr", "test", "test")) then: act.getType() == WsResponse.Type.SUBSCRIPTION - act.getValue() == new JavaHttpSubscriptionAdapter.SubscriptionResponse("EsqruyKPnZvPZ6fr", "test", "test") - act.asEvent() == new JavaHttpSubscriptionAdapter.SubscriptionResponse("EsqruyKPnZvPZ6fr", "test", "test") + act.getValue() == new SubscriptionResponse("EsqruyKPnZvPZ6fr", "test", "test") + act.asEvent() == new 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 JavaHttpSubscriptionAdapter.SubscriptionResponse("EsqruyKPnZvPZ6fr", "test", "test")).asRpc() + WsResponse.subscription(new SubscriptionResponse("EsqruyKPnZvPZ6fr", "test", "test")).asRpc() then: thrown(ClassCastException) } diff --git a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt index f4dc1f3..12f3730 100644 --- a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt +++ b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt @@ -6,6 +6,7 @@ import io.emeraldpay.polkaj.api.RpcCall import io.emeraldpay.polkaj.api.RpcCallAdapter import io.emeraldpay.polkaj.api.RpcCoder import io.emeraldpay.polkaj.api.RpcException +import io.emeraldpay.polkaj.apiokhttp.OkHttpRpcAdapter.Companion.APPLICATION_JSON import io.emeraldpay.polkaj.json.jackson.PolkadotModule import kotlinx.coroutines.* import kotlinx.coroutines.future.future @@ -19,8 +20,6 @@ import java.time.Duration import java.util.* import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletionException -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -33,8 +32,8 @@ class OkHttpRpcAdapter( private val onClose : () -> Unit ) : RpcCallAdapter { - companion object{ - private const val APPLICATION_JSON = "application/json" + internal companion object{ + const val APPLICATION_JSON = "application/json" } private var closed = false @@ -67,9 +66,9 @@ class OkHttpRpcAdapter( } } - fun getCall(rpcCall: RpcCall) : Call{ - val id = rpcCoder.nextId() - val type = rpcCall.getResultType(rpcCoder.objectMapper.typeFactory) + fun nextId() : Int = rpcCoder.nextId() + + fun getCall(id: Int, rpcCall: RpcCall) : Call { return baseRequest.newBuilder().post( rpcCoder.encode(id, rpcCall).toRequestBody(APPLICATION_JSON.toMediaType()) ).build().let { @@ -77,53 +76,12 @@ class OkHttpRpcAdapter( } } - suspend fun await(rpcCall : RpcCall): T { - return suspendCancellableCoroutine { continuation -> - val id = rpcCoder.nextId() - val type = rpcCall.getResultType(rpcCoder.objectMapper.typeFactory) - val call = baseRequest.newBuilder().post( - rpcCoder.encode(id, rpcCall).toRequestBody(APPLICATION_JSON.toMediaType()) - ).build().let { - client.newCall(it) - } - continuation.invokeOnCancellation { - call.cancel() - } - call.enqueue(object : Callback{ - override fun onFailure(call: Call, e: IOException) { - continuation.resumeWithException(e) - } - - override fun onResponse(call: Call, response: Response) { - if(response.code != 200){ - continuation.resumeWithException(RpcException( - -32000, "Server returned error status: ${response.code}" - )) - } else if(response.header("content-type", APPLICATION_JSON)?.startsWith(APPLICATION_JSON) == false){ - continuation.resumeWithException(RpcException( - -32000, "Server returned invalid content-type ${response.header("content-type")}" - )) - } else{ - try{ - rpcCoder.decode(id, response.body!!.byteStream(), type).let { - continuation.resume(it) - } - }catch (e : Throwable){ - when(e){ - is JsonProcessingException -> continuation.resumeWithException(RpcException(-32600, "Unable to encode request as JSON: ${e.message}")) - is CompletionException -> continuation.resumeWithException(e.cause ?: e) - else -> continuation.resumeWithException(e) - } - - } - } - } - }) - - } + fun decodeResponse(id : Int, rpcCall: RpcCall, response: Response) : T { + val type = rpcCall.getResultType(rpcCoder.objectMapper.typeFactory) + return rpcCoder.decode(id, response.body!!.byteStream(), type) } - data class Builder( + data class Builder( private var target : HttpUrl = "http://127.0.0.1:9933".toHttpUrl(), private var basicAuth : String? = null, private var client : OkHttpClient = OkHttpClient.Builder().apply { @@ -161,4 +119,45 @@ class OkHttpRpcAdapter( fun build() : OkHttpRpcAdapter = OkHttpRpcAdapter(target, basicAuth, client, scope, rpcCoder, onClose) } +} + +suspend fun OkHttpRpcAdapter.await(rpcCall : RpcCall): T { + return suspendCancellableCoroutine { continuation -> + val id = nextId() + val call = getCall(id, rpcCall) + continuation.invokeOnCancellation { + call.cancel() + } + call.enqueue(object : Callback{ + override fun onFailure(call: Call, e: IOException) { + continuation.resumeWithException(e) + } + + override fun onResponse(call: Call, response: Response) { + if(response.code != 200){ + continuation.resumeWithException(RpcException( + -32000, "Server returned error status: ${response.code}" + )) + } else if(response.header("content-type", APPLICATION_JSON)?.startsWith(APPLICATION_JSON) == false){ + continuation.resumeWithException(RpcException( + -32000, "Server returned invalid content-type ${response.header("content-type")}" + )) + } else{ + try{ + decodeResponse(id, rpcCall, response).let { + continuation.resume(it) + } + }catch (e : Throwable){ + when(e){ + is JsonProcessingException -> continuation.resumeWithException(RpcException(-32600, "Unable to encode request as JSON: ${e.message}")) + is CompletionException -> continuation.resumeWithException(e.cause ?: e) + else -> continuation.resumeWithException(e) + } + + } + } + } + }) + + } } \ No newline at end of file diff --git a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt new file mode 100644 index 0000000..4ee8c22 --- /dev/null +++ b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt @@ -0,0 +1,294 @@ +package io.emeraldpay.polkaj.apiokhttp + +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.polkaj.api.* +import io.emeraldpay.polkaj.api.internal.DecodeResponse +import io.emeraldpay.polkaj.api.internal.WsResponse +import io.emeraldpay.polkaj.json.jackson.PolkadotModule +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.future.asCompletableFuture +import okhttp3.* +import java.time.Duration +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.function.Consumer + +class OkHttpSubscriptionAdapter( + private val target : String, + private val basicAuth : String?, + private val client : OkHttpClient, + private val scope : CoroutineScope, + private val rpcCoder: RpcCoder, + private val onClose : () -> Unit +) : SubscriptionAdapter { + + private data class State( + val socketState : SocketState = SocketState.Idle, + val rpcCalls : List> = emptyList(), + val subscriptionCalls : List> = emptyList(), + val curSocketJob : Job? = null + ) + + private sealed class SocketState { + object Idle : SocketState() + object Connecting : SocketState() + data class Connected(val webSocket: WebSocket) : SocketState() + object Closing : SocketState() + object Closed : SocketState() + data class Failed(val throwable: Throwable?) : SocketState() + } + + private data class RpcDeferred( + val id : Int, + val call: RpcCall, + val deferred: CompletableDeferred + ) + + private class WebscoketFailedException(cause : Throwable?) : Exception(cause) + + @Suppress("BlockingMethodInNonBlockingContext") + class FlowSubscription( + val id : String, + val call: SubscribeCall, + private val scope: CoroutineScope, + private val events : Flow>, + private val onClose: (FlowSubscription) -> Unit) : Subscription { + + private var job : Job? = null + + override fun handler(handler: Consumer>?) { + job?.cancel() + if(handler == null) return + @Suppress("UNCHECKED_CAST") + val consumer = handler as Consumer> + job = events.onEach { + consumer.accept(it) + }.launchIn(scope) + } + + override fun close() { + job?.cancel() + onClose(this) + } + } + + private val _state = MutableStateFlow(State()) + private val _messages = MutableSharedFlow(1) + + private val curState = _state.asStateFlow() + private val messages = _messages.asSharedFlow() + private val rpcEvents = messages.filter { it.type == WsResponse.Type.RPC }.map { it.asRpc() } + private val subscriptionEvents = messages.filter { it.type == WsResponse.Type.SUBSCRIPTION }.map { it.asEvent() } + private val decodeResponse : DecodeResponse + + init { + val rpcMapping = { id : Int -> + curState.value.rpcCalls.firstOrNull { it.id == id }?.call?.getResultType(rpcCoder.objectMapper.typeFactory) + } + val subMapping = { id : String -> + curState.value.subscriptionCalls.firstOrNull { it.id == id }?.call?.getResultType(rpcCoder.objectMapper.typeFactory) + } + decodeResponse = DecodeResponse(rpcCoder.objectMapper, rpcMapping, subMapping) + } + + override fun close() { + curState.value.socketState.let { + if(it is SocketState.Connected) it.webSocket.close(1000, "close") + } + onClose() + } + + @Suppress("UNCHECKED_CAST", "BlockingMethodInNonBlockingContext") + override fun produceRpcFuture(call: RpcCall): CompletableFuture { + val result = CompletableDeferred() + val exHandler = CoroutineExceptionHandler { _, throwable -> + result.completeExceptionally(throwable) + } + scope.launch(Dispatchers.IO + exHandler) { + val id = rpcCoder.nextId() + val payload = try { + rpcCoder.encode(id, call) + } catch (e : JsonProcessingException){ + result.completeExceptionally(e) + null + } + if(payload != null) { + val rpc = RpcDeferred(id, call, result) + _state += rpc + curState.socket().send(String(payload)) + rpcEvents.first { it.id == id }.let { + if(it.error != null) result.completeExceptionally(RpcException(it.error.code, it.error.message, it.error.data)) + else result.complete(it.result as T) + }.also { + _state -= rpc + } + } + } + return result.asCompletableFuture() + } + + @Suppress("UNCHECKED_CAST") + override fun subscribe(call: SubscribeCall): CompletableFuture> { + val start = RpcCall.create(String::class.java, call.method, *call.params) + return produceRpcFuture(start).thenApply { id -> + val events = subscriptionEvents.filter { it.id == id }.map { Subscription.Event(it.method, it.value as T) } + FlowSubscription(id, call, scope, events) { + produceRpcFuture(RpcCall.create(Boolean::class.java, call.unsubscribe, id)) + _state -= it + }.also { + _state += it + } + } + } + + private fun createSocket() : Flow { + val request = Request.Builder().apply { + url(target) + header("User-Agent", "PolkaJ/OkHttp/0.5") + header("Content-Type", OkHttpRpcAdapter.APPLICATION_JSON) + if(basicAuth != null) header("Authorization", basicAuth) + }.build() + + return callbackFlow { + trySend(SocketState.Connecting) + val webSocket = client.newWebSocket(request, object : WebSocketListener(){ + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { + trySend(SocketState.Closed) + close() + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + trySend(SocketState.Closing) + close() + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + t.printStackTrace() + trySend(SocketState.Failed(t)) + close() + } + + override fun onMessage(webSocket: WebSocket, text: String) { + scope.launch(Dispatchers.IO) { _messages.emit(decodeResponse.decode(text)) } + } + + override fun onOpen(webSocket: WebSocket, response: Response) { + trySend(SocketState.Connected(webSocket)) + } + }) + + awaitClose { + if(curState.value.socketState == SocketState.Connecting) webSocket.cancel() + } + + }.buffer(Channel.UNLIMITED).onEach { + when(it){ + SocketState.Closed -> _state += SocketState.Idle + SocketState.Closing -> _state += SocketState.Idle + is SocketState.Connected -> _state += it + SocketState.Connecting -> _state += it + is SocketState.Failed -> { + val exception = WebscoketFailedException(it.throwable) + _state.update { state -> + state.rpcCalls.forEach { call-> call.deferred.completeExceptionally(exception) } + //TODO update subscription handler to take error events and report error here and/or reconnect + state.copy(socketState = SocketState.Idle, rpcCalls = emptyList(), subscriptionCalls = emptyList()) + } + } + SocketState.Idle -> _state += it + } + } + } + + private fun createSocketIfNeeded() = _state.update { + when(it.socketState){ + is SocketState.Connected, SocketState.Connecting -> { it } + else -> { + it.curSocketJob?.cancel() + //TODO add catch and exception handlers in all launch/launchin + val newJob = createSocket().launchIn(scope) + it.copy(curSocketJob = newJob) + } + } + } + + private suspend fun StateFlow.socket() : WebSocket = also { + createSocketIfNeeded() + }.map { + it.socketState + }.transform { + if(it is SocketState.Connected) emit(it.webSocket) + }.first() + + private operator fun MutableStateFlow.plusAssign(rpcDeferred : RpcDeferred) { + _state.update { + it.copy(rpcCalls = it.rpcCalls + rpcDeferred) + } + } + + private operator fun MutableStateFlow.minusAssign(rpcDeferred : RpcDeferred) { + _state.update { + it.copy(rpcCalls = it.rpcCalls - rpcDeferred) + } + } + + private operator fun MutableStateFlow.plusAssign(socketState: SocketState) { + _state.update { + it.copy(socketState = socketState) + } + } + + private operator fun MutableStateFlow.plusAssign(subscription : FlowSubscription) { + _state.update { + it.copy(subscriptionCalls = it.subscriptionCalls + subscription) + } + } + + private operator fun MutableStateFlow.minusAssign(subscription : FlowSubscription) { + _state.update { + it.copy(subscriptionCalls = it.subscriptionCalls - subscription) + } + } + + data class Builder( + private var target : String = "ws://127.0.0.1:9944", + private var basicAuth : String? = null, + private var client : OkHttpClient = OkHttpClient.Builder().apply { + callTimeout(Duration.ofMinutes(1)) + followRedirects(false) + }.build(), + private var scope : CoroutineScope = CoroutineScope(SupervisorJob()), + private var rpcCoder: RpcCoder = RpcCoder(ObjectMapper().registerModule(PolkadotModule())), + private var onClose : () -> Unit = { + scope.cancel() + client.dispatcher.executorService.shutdown() + } + ){ + companion object{ + inline operator fun invoke(block : Builder.() -> Builder) : OkHttpSubscriptionAdapter { + return Builder().apply { block() }.build() + } + + } + + fun target(target: String) = apply { this.target = target } + + fun basicAuth(username: String, password: String) : Builder{ + return apply { + val combine = "$username:$password".toByteArray() + basicAuth = "Basic ${Base64.getEncoder().encodeToString(combine)}" + } + } + + fun client(client : OkHttpClient) = apply { this.client = client } + fun scope(scope : CoroutineScope) = apply { this.scope = scope } + fun rpcCoder(rpcCoder : RpcCoder) = apply { this.rpcCoder = rpcCoder } + fun onClose(block : () -> Unit ) = apply { onClose = block } + fun timeout(timeout : Duration) = apply { client = client.newBuilder().callTimeout(timeout).build() } + fun build() : OkHttpSubscriptionAdapter = OkHttpSubscriptionAdapter(target, basicAuth, client, scope, rpcCoder, onClose) + } +} \ No newline at end of file diff --git a/polkaj-api-okhttp/src/test/groovy/OkHttpSubscriptionAdapterSpec.groovy b/polkaj-api-okhttp/src/test/groovy/OkHttpSubscriptionAdapterSpec.groovy new file mode 100644 index 0000000..7825404 --- /dev/null +++ b/polkaj-api-okhttp/src/test/groovy/OkHttpSubscriptionAdapterSpec.groovy @@ -0,0 +1,16 @@ +import io.emeraldpay.polkaj.api.SubscriptionAdapter +import io.emeraldpay.polkaj.api.SubscriptionAdapterSpec +import io.emeraldpay.polkaj.apiokhttp.OkHttpSubscriptionAdapter + +import java.time.Duration + +class OkHttpSubscriptionAdapterSpec extends SubscriptionAdapterSpec{ + + @Override + SubscriptionAdapter provideAdapter(String connectTo) { + return OkHttpSubscriptionAdapter.Builder.@Companion.invoke({ builder -> + builder.target(connectTo) + .timeout(Duration.ofSeconds(15)) + }) + } +} diff --git a/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/JavaHttpSubscriptionAdapter.java b/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/JavaHttpSubscriptionAdapter.java index 7b15b52..9760e8e 100644 --- a/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/JavaHttpSubscriptionAdapter.java +++ b/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/JavaHttpSubscriptionAdapter.java @@ -4,6 +4,9 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import io.emeraldpay.polkaj.api.*; +import io.emeraldpay.polkaj.api.internal.DecodeResponse; +import io.emeraldpay.polkaj.api.internal.SubscriptionResponse; +import io.emeraldpay.polkaj.api.internal.WsResponse; import io.emeraldpay.polkaj.json.jackson.PolkadotModule; import java.net.URI; @@ -12,7 +15,6 @@ import java.net.http.WebSocket; import java.nio.ByteBuffer; import java.time.Duration; -import java.util.Objects; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; @@ -193,11 +195,11 @@ public void accept(RpcResponse response) { @SuppressWarnings("unchecked") public void accept(SubscriptionResponse response) { - DefaultSubscription s = (DefaultSubscription) subscriptions.get(response.id); + DefaultSubscription s = (DefaultSubscription) subscriptions.get(response.getId()); if (s == null) { return; } - s.accept(new Subscription.Event<>(response.method, response.value)); + s.accept(new Subscription.Event<>(response.getMethod(), response.getValue())); } public boolean removeSubscription(String id) { @@ -224,45 +226,6 @@ public void close() { } } - static class SubscriptionResponse { - private final String id; - private final String method; - private final T value; - - public SubscriptionResponse(String id, String method, T value) { - this.id = id; - this.method = method; - this.value = value; - } - - public String getId() { - return id; - } - - public String getMethod() { - return method; - } - - public T getValue() { - return value; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof SubscriptionResponse)) return false; - SubscriptionResponse that = (SubscriptionResponse) o; - return id.equals(that.id) && - Objects.equals(method, that.method) && - Objects.equals(value, that.value); - } - - @Override - public int hashCode() { - return Objects.hash(id, method, value); - } - } - static class RequestExpectation { private final JavaType type; private final CompletableFuture handler; From f0be3ca421c961cbdf23bde53896907566878a59 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Sat, 25 Sep 2021 03:31:54 -0500 Subject: [PATCH 34/38] add test coverage --- polkaj-api-okhttp/build.gradle | 6 +- .../apiokhttp/OkHttpSubscriptionAdapter.kt | 40 +++-- .../apiokhttp}/OkHttpRpcAdapterSpec.groovy | 3 +- .../OkHttpSubscriptionAdapterSpec.groovy | 10 +- .../polkaj/OkHttpSubscriptionAdapterTests.kt | 138 ++++++++++++++++++ 5 files changed, 174 insertions(+), 23 deletions(-) rename polkaj-api-okhttp/src/test/groovy/{ => io/emeraldpay/polkaj/apiokhttp}/OkHttpRpcAdapterSpec.groovy (90%) rename polkaj-api-okhttp/src/test/groovy/{ => io/emeraldpay/polkaj/apiokhttp}/OkHttpSubscriptionAdapterSpec.groovy (78%) create mode 100644 polkaj-api-okhttp/src/test/kotlin/io/emeraldpay/polkaj/OkHttpSubscriptionAdapterTests.kt diff --git a/polkaj-api-okhttp/build.gradle b/polkaj-api-okhttp/build.gradle index 0ccdf56..151a906 100644 --- a/polkaj-api-okhttp/build.gradle +++ b/polkaj-api-okhttp/build.gradle @@ -18,8 +18,6 @@ compileJava { targetCompatibility = JavaVersion.VERSION_1_8 } - - dependencies { api project(":polkaj-json-types") api project(":polkaj-api-base") @@ -30,4 +28,8 @@ dependencies { testImplementation 'org.java-websocket:Java-WebSocket:1.5.1' testImplementation project(":polkaj-adapter-tests") + testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlin_coroutine_version" + testImplementation("org.junit.jupiter:junit-jupiter-params:5.8.0") + testImplementation "io.mockk:mockk:1.12.0" } \ No newline at end of file diff --git a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt index 4ee8c22..0d30f3a 100644 --- a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt +++ b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt @@ -51,7 +51,7 @@ class OkHttpSubscriptionAdapter( private class WebscoketFailedException(cause : Throwable?) : Exception(cause) @Suppress("BlockingMethodInNonBlockingContext") - class FlowSubscription( + private class FlowSubscription( val id : String, val call: SubscribeCall, private val scope: CoroutineScope, @@ -128,6 +128,7 @@ class OkHttpSubscriptionAdapter( } } } + return result.asCompletableFuture() } @@ -167,13 +168,17 @@ class OkHttpSubscriptionAdapter( } override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { - t.printStackTrace() + t.printStackTrace(System.err) trySend(SocketState.Failed(t)) close() } override fun onMessage(webSocket: WebSocket, text: String) { - scope.launch(Dispatchers.IO) { _messages.emit(decodeResponse.decode(text)) } + val handler = CoroutineExceptionHandler{ _, throwable -> + throwable.printStackTrace(System.err) + } + @Suppress("BlockingMethodInNonBlockingContext") + scope.launch(Dispatchers.IO + handler) { _messages.emit(decodeResponse.decode(text)) } } override fun onOpen(webSocket: WebSocket, response: Response) { @@ -191,14 +196,7 @@ class OkHttpSubscriptionAdapter( SocketState.Closing -> _state += SocketState.Idle is SocketState.Connected -> _state += it SocketState.Connecting -> _state += it - is SocketState.Failed -> { - val exception = WebscoketFailedException(it.throwable) - _state.update { state -> - state.rpcCalls.forEach { call-> call.deferred.completeExceptionally(exception) } - //TODO update subscription handler to take error events and report error here and/or reconnect - state.copy(socketState = SocketState.Idle, rpcCalls = emptyList(), subscriptionCalls = emptyList()) - } - } + is SocketState.Failed -> handleSocketException(it.throwable) SocketState.Idle -> _state += it } } @@ -209,13 +207,23 @@ class OkHttpSubscriptionAdapter( is SocketState.Connected, SocketState.Connecting -> { it } else -> { it.curSocketJob?.cancel() - //TODO add catch and exception handlers in all launch/launchin - val newJob = createSocket().launchIn(scope) + val newJob = createSocket().catch { error -> + handleSocketException(error) + }.launchIn(scope) it.copy(curSocketJob = newJob) } } } + private fun handleSocketException(t : Throwable?){ + val exception = WebscoketFailedException(t) + _state.update { state -> + state.rpcCalls.forEach { call-> call.deferred.completeExceptionally(exception) } + //TODO update subscription handler to take error events and report error here + state.copy(socketState = SocketState.Idle, rpcCalls = emptyList(), subscriptionCalls = emptyList()) + } + } + private suspend fun StateFlow.socket() : WebSocket = also { createSocketIfNeeded() }.map { @@ -254,7 +262,7 @@ class OkHttpSubscriptionAdapter( } } - data class Builder( + class Builder private constructor( private var target : String = "ws://127.0.0.1:9944", private var basicAuth : String? = null, private var client : OkHttpClient = OkHttpClient.Builder().apply { @@ -269,7 +277,7 @@ class OkHttpSubscriptionAdapter( } ){ companion object{ - inline operator fun invoke(block : Builder.() -> Builder) : OkHttpSubscriptionAdapter { + operator fun invoke(block : Builder.() -> Unit) : OkHttpSubscriptionAdapter { return Builder().apply { block() }.build() } @@ -289,6 +297,6 @@ class OkHttpSubscriptionAdapter( fun rpcCoder(rpcCoder : RpcCoder) = apply { this.rpcCoder = rpcCoder } fun onClose(block : () -> Unit ) = apply { onClose = block } fun timeout(timeout : Duration) = apply { client = client.newBuilder().callTimeout(timeout).build() } - fun build() : OkHttpSubscriptionAdapter = OkHttpSubscriptionAdapter(target, basicAuth, client, scope, rpcCoder, onClose) + private fun build() : OkHttpSubscriptionAdapter = OkHttpSubscriptionAdapter(target, basicAuth, client, scope, rpcCoder, onClose) } } \ No newline at end of file diff --git a/polkaj-api-okhttp/src/test/groovy/OkHttpRpcAdapterSpec.groovy b/polkaj-api-okhttp/src/test/groovy/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapterSpec.groovy similarity index 90% rename from polkaj-api-okhttp/src/test/groovy/OkHttpRpcAdapterSpec.groovy rename to polkaj-api-okhttp/src/test/groovy/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapterSpec.groovy index a767607..edfee58 100644 --- a/polkaj-api-okhttp/src/test/groovy/OkHttpRpcAdapterSpec.groovy +++ b/polkaj-api-okhttp/src/test/groovy/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapterSpec.groovy @@ -1,6 +1,7 @@ +package io.emeraldpay.polkaj.apiokhttp + import io.emeraldpay.polkaj.api.RpcAdapterSpec import io.emeraldpay.polkaj.api.RpcCallAdapter -import io.emeraldpay.polkaj.apiokhttp.OkHttpRpcAdapter import java.time.Duration diff --git a/polkaj-api-okhttp/src/test/groovy/OkHttpSubscriptionAdapterSpec.groovy b/polkaj-api-okhttp/src/test/groovy/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapterSpec.groovy similarity index 78% rename from polkaj-api-okhttp/src/test/groovy/OkHttpSubscriptionAdapterSpec.groovy rename to polkaj-api-okhttp/src/test/groovy/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapterSpec.groovy index 7825404..e0a81ac 100644 --- a/polkaj-api-okhttp/src/test/groovy/OkHttpSubscriptionAdapterSpec.groovy +++ b/polkaj-api-okhttp/src/test/groovy/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapterSpec.groovy @@ -1,16 +1,18 @@ +package io.emeraldpay.polkaj.apiokhttp + import io.emeraldpay.polkaj.api.SubscriptionAdapter import io.emeraldpay.polkaj.api.SubscriptionAdapterSpec import io.emeraldpay.polkaj.apiokhttp.OkHttpSubscriptionAdapter import java.time.Duration -class OkHttpSubscriptionAdapterSpec extends SubscriptionAdapterSpec{ +class OkHttpSubscriptionAdapterSpec extends SubscriptionAdapterSpec { @Override SubscriptionAdapter provideAdapter(String connectTo) { return OkHttpSubscriptionAdapter.Builder.@Companion.invoke({ builder -> - builder.target(connectTo) - .timeout(Duration.ofSeconds(15)) + builder.target(connectTo).timeout(Duration.ofSeconds(15)) }) } -} + +} \ No newline at end of file diff --git a/polkaj-api-okhttp/src/test/kotlin/io/emeraldpay/polkaj/OkHttpSubscriptionAdapterTests.kt b/polkaj-api-okhttp/src/test/kotlin/io/emeraldpay/polkaj/OkHttpSubscriptionAdapterTests.kt new file mode 100644 index 0000000..d9aad69 --- /dev/null +++ b/polkaj-api-okhttp/src/test/kotlin/io/emeraldpay/polkaj/OkHttpSubscriptionAdapterTests.kt @@ -0,0 +1,138 @@ +package io.emeraldpay.polkaj + +import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.polkaj.api.RpcCall +import io.emeraldpay.polkaj.api.RpcCoder +import io.emeraldpay.polkaj.apiokhttp.OkHttpSubscriptionAdapter +import io.emeraldpay.polkaj.json.jackson.PolkadotModule +import io.mockk.* +import kotlinx.coroutines.test.TestCoroutineScope +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.WebSocket +import okhttp3.WebSocketListener +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.time.Duration +import java.util.concurrent.ExecutionException +import kotlin.test.assertEquals + +class OkHttpSubscriptionAdapterTests { + + private val target = "ws://localhost:9999" + private val scope = TestCoroutineScope() + private val rpcCoder = mockk(relaxed = true){ + every { objectMapper } returns ObjectMapper().apply { registerModule(PolkadotModule()) } + } + private val client = mockk(relaxed = true) + private val onClose = spyk({}) + private val webSocket = mockk() + + private val listener = CapturingSlot() + private val request = CapturingSlot() + + private val finalizedHeadResponse = """{ + "jsonrpc" : "2.0", + "result" : "0x5d83f66b61701da4cbd7a60137db89c69469a4f798b62aba9176ab253b423828", + "id" : 0 + } + """.trimIndent() + + private val subscriptionAdapter = OkHttpSubscriptionAdapter.Builder { + timeout(Duration.ofSeconds(1)) + target(target) + scope(scope) + basicAuth("alice", "secret") + rpcCoder(rpcCoder) + client(client) + onClose(onClose) + } + + @BeforeEach + fun setup(){ + every { + client.newWebSocket(capture(request), capture(listener)) + } answers { + listener.captured.onOpen(webSocket, mockk()) + webSocket + } + + every { rpcCoder.nextId() } returns 0 + } + + @AfterEach + fun cleanup(){ + scope.cleanupTestCoroutines() + } + + @Test + fun `client connects automatically`(){ + mockResponse() + val call = RpcCall.create(String::class.java, "chain_getFinalisedHead") + subscriptionAdapter.produceRpcFuture(call).get() + verify(exactly = 1) { + client.newWebSocket(any(), any()) + } + } + + @Test + fun `client doesn't make a second websocket when already connected`(){ + mockResponse() + val call = RpcCall.create(String::class.java, "chain_getFinalisedHead") + subscriptionAdapter.produceRpcFuture(call).get() + subscriptionAdapter.produceRpcFuture(call).get() + verify(exactly = 1) { + client.newWebSocket(any(), any()) + } + } + + @Test + fun `adapter closes given onClose method`(){ + mockResponse() + every { webSocket.close(any(), any()) } returns true + val call = RpcCall.create(String::class.java, "chain_getFinalisedHead") + subscriptionAdapter.produceRpcFuture(call).get() + subscriptionAdapter.close() + verify { + onClose.invoke() + webSocket.close(eq(1000), eq("close")) + } + } + + @Test + fun `client uses basic auth`(){ + mockResponse() + val call = RpcCall.create(String::class.java, "chain_getFinalisedHead") + subscriptionAdapter.produceRpcFuture(call).get() + assertEquals("Basic YWxpY2U6c2VjcmV0", request.captured.headers["Authorization"]) + } + + @Test + fun `socket failure given to pending request`(){ + val exception = Exception() + val call = RpcCall.create(String::class.java, "chain_getFinalisedHead") + every { webSocket.send(any()) } answers { + listener.captured.onMessage(webSocket, finalizedHeadResponse) + true + } andThenAnswer { + listener.captured.onFailure(webSocket, exception, null) + true + } + subscriptionAdapter.produceRpcFuture(call).get() + val future = subscriptionAdapter.produceRpcFuture(call) + + val caught = assertThrows { + future.get() + } + assertEquals(exception, caught.cause?.cause) + } + + private fun mockResponse(){ + every { webSocket.send(any()) } answers { + listener.captured.onMessage(webSocket, finalizedHeadResponse) + true + } + } +} \ No newline at end of file From b7092878cfea80f46bafdd00dd5ff42f338c0919 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Mon, 27 Sep 2021 22:12:50 -0500 Subject: [PATCH 35/38] Increase code coverage --- .github/workflows/test.yaml | 6 +- .../emeraldpay/polkaj/apiokhttp/Constants.kt | 5 ++ .../polkaj/apiokhttp/OkHttpRpcAdapter.kt | 18 ++-- .../apiokhttp/OkHttpSubscriptionAdapter.kt | 15 +++- .../apiokhttp/OkHttpRpcAdapterSpec.groovy | 7 +- .../OkHttpSubscriptionAdapterSpec.groovy | 4 +- .../polkaj/OkHttpRpcAdapterTests.kt | 88 +++++++++++++++++++ 7 files changed, 123 insertions(+), 20 deletions(-) create mode 100644 polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/Constants.kt create mode 100644 polkaj-api-okhttp/src/test/kotlin/io/emeraldpay/polkaj/OkHttpRpcAdapterTests.kt diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 74bc99d..c47ae02 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -89,9 +89,9 @@ jobs: matrix: api-level: [24, 25, 26, 28, 29, 30] arch: [x86_64, x86] - include: - - api-level: 30 - arch: arm64-v8a +# include: +# - api-level: 30 +# arch: arm64-v8a ## Currently this github action isn't able to start any arm emulators steps: - name: checkout diff --git a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/Constants.kt b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/Constants.kt new file mode 100644 index 0000000..2658f6a --- /dev/null +++ b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/Constants.kt @@ -0,0 +1,5 @@ +package io.emeraldpay.polkaj.apiokhttp + +internal object Constants { + const val APPLICATION_JSON = "application/json" +} \ No newline at end of file diff --git a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt index 12f3730..17dbad7 100644 --- a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt +++ b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt @@ -6,7 +6,7 @@ import io.emeraldpay.polkaj.api.RpcCall import io.emeraldpay.polkaj.api.RpcCallAdapter import io.emeraldpay.polkaj.api.RpcCoder import io.emeraldpay.polkaj.api.RpcException -import io.emeraldpay.polkaj.apiokhttp.OkHttpRpcAdapter.Companion.APPLICATION_JSON +import io.emeraldpay.polkaj.apiokhttp.Constants.APPLICATION_JSON import io.emeraldpay.polkaj.json.jackson.PolkadotModule import kotlinx.coroutines.* import kotlinx.coroutines.future.future @@ -23,7 +23,7 @@ import java.util.concurrent.CompletionException import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException -class OkHttpRpcAdapter( +class OkHttpRpcAdapter private constructor( private val target : HttpUrl, private val basicAuth : String?, private val client : OkHttpClient, @@ -32,8 +32,9 @@ class OkHttpRpcAdapter( private val onClose : () -> Unit ) : RpcCallAdapter { - internal companion object{ - const val APPLICATION_JSON = "application/json" + companion object{ + @JvmStatic + fun newBuilder() : Builder = Builder() } private var closed = false @@ -78,10 +79,10 @@ class OkHttpRpcAdapter( fun decodeResponse(id : Int, rpcCall: RpcCall, response: Response) : T { val type = rpcCall.getResultType(rpcCoder.objectMapper.typeFactory) - return rpcCoder.decode(id, response.body!!.byteStream(), type) + return rpcCoder.decode(id, response.body!!.byteStream(), type) } - data class Builder( + class Builder private constructor( private var target : HttpUrl = "http://127.0.0.1:9933".toHttpUrl(), private var basicAuth : String? = null, private var client : OkHttpClient = OkHttpClient.Builder().apply { @@ -96,10 +97,13 @@ class OkHttpRpcAdapter( } ){ companion object{ - inline operator fun invoke(block : Builder.() -> Builder) : OkHttpRpcAdapter { + operator fun invoke(block : Builder.() -> Builder) : OkHttpRpcAdapter { return Builder().apply { block() }.build() } + operator fun invoke() : Builder{ + return Builder() + } } fun target(target: String) = apply { this.target = target.toHttpUrl() } diff --git a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt index 0d30f3a..7998b45 100644 --- a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt +++ b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.polkaj.api.* import io.emeraldpay.polkaj.api.internal.DecodeResponse import io.emeraldpay.polkaj.api.internal.WsResponse +import io.emeraldpay.polkaj.apiokhttp.Constants.APPLICATION_JSON import io.emeraldpay.polkaj.json.jackson.PolkadotModule import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel @@ -17,7 +18,7 @@ import java.util.* import java.util.concurrent.CompletableFuture import java.util.function.Consumer -class OkHttpSubscriptionAdapter( +class OkHttpSubscriptionAdapter private constructor( private val target : String, private val basicAuth : String?, private val client : OkHttpClient, @@ -26,6 +27,11 @@ class OkHttpSubscriptionAdapter( private val onClose : () -> Unit ) : SubscriptionAdapter { + companion object{ + @JvmStatic + fun newBuilder() : Builder = Builder() + } + private data class State( val socketState : SocketState = SocketState.Idle, val rpcCalls : List> = emptyList(), @@ -150,7 +156,7 @@ class OkHttpSubscriptionAdapter( val request = Request.Builder().apply { url(target) header("User-Agent", "PolkaJ/OkHttp/0.5") - header("Content-Type", OkHttpRpcAdapter.APPLICATION_JSON) + header("Content-Type", APPLICATION_JSON) if(basicAuth != null) header("Authorization", basicAuth) }.build() @@ -281,6 +287,9 @@ class OkHttpSubscriptionAdapter( return Builder().apply { block() }.build() } + operator fun invoke() : Builder { + return Builder() + } } fun target(target: String) = apply { this.target = target } @@ -297,6 +306,6 @@ class OkHttpSubscriptionAdapter( fun rpcCoder(rpcCoder : RpcCoder) = apply { this.rpcCoder = rpcCoder } fun onClose(block : () -> Unit ) = apply { onClose = block } fun timeout(timeout : Duration) = apply { client = client.newBuilder().callTimeout(timeout).build() } - private fun build() : OkHttpSubscriptionAdapter = OkHttpSubscriptionAdapter(target, basicAuth, client, scope, rpcCoder, onClose) + fun build() : OkHttpSubscriptionAdapter = OkHttpSubscriptionAdapter(target, basicAuth, client, scope, rpcCoder, onClose) } } \ No newline at end of file diff --git a/polkaj-api-okhttp/src/test/groovy/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapterSpec.groovy b/polkaj-api-okhttp/src/test/groovy/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapterSpec.groovy index edfee58..fb87940 100644 --- a/polkaj-api-okhttp/src/test/groovy/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapterSpec.groovy +++ b/polkaj-api-okhttp/src/test/groovy/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapterSpec.groovy @@ -9,10 +9,9 @@ class OkHttpRpcAdapterSpec extends RpcAdapterSpec { @Override RpcCallAdapter provideAdapter(String connectTo, String username, String password, Duration timeout) { - return OkHttpRpcAdapter.Builder.@Companion.invoke({ builder -> - builder.target(connectTo) + def builder = OkHttpRpcAdapter.newBuilder() + return builder.target(connectTo) .timeout(timeout) - .basicAuth(username, password) - }) + .basicAuth(username, password).build() } } diff --git a/polkaj-api-okhttp/src/test/groovy/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapterSpec.groovy b/polkaj-api-okhttp/src/test/groovy/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapterSpec.groovy index e0a81ac..5ad5b66 100644 --- a/polkaj-api-okhttp/src/test/groovy/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapterSpec.groovy +++ b/polkaj-api-okhttp/src/test/groovy/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapterSpec.groovy @@ -10,9 +10,7 @@ class OkHttpSubscriptionAdapterSpec extends SubscriptionAdapterSpec { @Override SubscriptionAdapter provideAdapter(String connectTo) { - return OkHttpSubscriptionAdapter.Builder.@Companion.invoke({ builder -> - builder.target(connectTo).timeout(Duration.ofSeconds(15)) - }) + return OkHttpSubscriptionAdapter.newBuilder().target(connectTo).timeout(Duration.ofSeconds(15)).build() } } \ No newline at end of file diff --git a/polkaj-api-okhttp/src/test/kotlin/io/emeraldpay/polkaj/OkHttpRpcAdapterTests.kt b/polkaj-api-okhttp/src/test/kotlin/io/emeraldpay/polkaj/OkHttpRpcAdapterTests.kt new file mode 100644 index 0000000..0ecc7b2 --- /dev/null +++ b/polkaj-api-okhttp/src/test/kotlin/io/emeraldpay/polkaj/OkHttpRpcAdapterTests.kt @@ -0,0 +1,88 @@ +package io.emeraldpay.polkaj + +import com.fasterxml.jackson.core.JsonGenerationException +import io.emeraldpay.polkaj.api.RpcCall +import io.emeraldpay.polkaj.api.RpcCoder +import io.emeraldpay.polkaj.api.RpcException +import io.emeraldpay.polkaj.apiokhttp.OkHttpRpcAdapter +import io.mockk.* +import kotlinx.coroutines.test.TestCoroutineScope +import okhttp3.Callback +import okhttp3.OkHttpClient +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.io.InputStream +import java.lang.IllegalStateException +import java.util.concurrent.ExecutionException +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class OkHttpRpcAdapterTests { + + private val target = "http://localhost:18080" + private val client = mockk() + private val scope = TestCoroutineScope() + private val rpcCoder = mockk(relaxed = true) + private val onClose = spyk({}) + private val adapter = OkHttpRpcAdapter.Builder{ + target(target) + client(client) + scope(scope) + rpcCoder(rpcCoder) + onClose(onClose) + basicAuth("alice", "secret") + } + + @Test + fun `the builder uses correct components`(){ + val responseString = "correct" + val call = RpcCall.create(String::class.java, "chain_getFinalisedHead") + val listener = CapturingSlot() + every { client.newCall(any()) } returns mockk(relaxed = true){ + every { enqueue(capture(listener)) } answers { + listener.captured.onResponse(mockk(), mockk(relaxed = true){ + every { code } returns 200 + every { header("content-type", any()) } returns "application/json" + }) + } + } + every { rpcCoder.decode(any(), any(), any()) } returns responseString + val response = adapter.produceRpcFuture(call).get() + adapter.close() + verify { + onClose.invoke() + } + assertEquals(responseString, response) + } + + @Test + fun `handles JsonProcessingException`(){ + val call = RpcCall.create(String::class.java, "chain_getFinalisedHead") + val listener = CapturingSlot() + every { client.newCall(any()) } returns mockk(relaxed = true){ + every { enqueue(capture(listener)) } answers { + listener.captured.onResponse(mockk(), mockk(relaxed = true){ + every { code } returns 200 + every { header("content-type", any()) } returns "application/json" + }) + } + } + every { rpcCoder.decode(any(), any(), any()) } throws JsonGenerationException("") + val throwable = assertThrows { + adapter.produceRpcFuture(call).get() + } + val cause = throwable.cause + assertIs(cause) + assertEquals(-32600, cause.code) + } + + @Test + fun `can not make a call after close`(){ + adapter.close() + val future = adapter.produceRpcFuture(RpcCall.create(String::class.java, "chain_getFinalisedHead")) + val throwable = assertThrows { + future.get() + } + assertIs(throwable.cause) + } +} \ No newline at end of file From b71e5aada20cd6ee87f7ed33235bd2df8334653f Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Tue, 5 Oct 2021 01:03:54 -0500 Subject: [PATCH 36/38] update documentation and examples --- README.adoc | 8 ++- examples/README.adoc | 12 +++- examples/balance/build.gradle | 1 + examples/balance/src/main/java/Balance.java | 9 ++- examples/balance/src/main/java/Transfer.java | 12 +++- examples/rpc/build.gradle | 3 +- .../rpc/src/main/java/DescribeRuntime.java | 8 ++- examples/rpc/src/main/java/FollowState.java | 12 +++- examples/rpc/src/main/java/ShowState.java | 20 ++---- polkaj-api-http/README.adoc | 6 +- polkaj-api-okhttp/README.adoc | 32 +++++++++ .../polkaj/apiokhttp/OkHttpRpcAdapter.kt | 50 ++++++++++++++ .../apiokhttp/OkHttpSubscriptionAdapter.kt | 65 +++++++++++++++---- .../OkHttpSubscriptionAdapterSpec.groovy | 3 +- .../polkaj/OkHttpSubscriptionAdapterTests.kt | 11 +--- .../apiws/JavaHttpSubscriptionAdapter.java | 4 +- polkaj-schnorrkel-android/README.adoc | 9 +++ 17 files changed, 208 insertions(+), 57 deletions(-) create mode 100644 polkaj-api-okhttp/README.adoc create mode 100644 polkaj-schnorrkel-android/README.adoc diff --git a/README.adoc b/README.adoc index bde2521..e7a238d 100644 --- a/README.adoc +++ b/README.adoc @@ -20,12 +20,14 @@ WARNING: UNDER DEVELOPMENT - `io.emeraldpay.polkaj:polkaj-scale:{lib-version}` - SCALE codec implementation - `io.emeraldpay.polkaj:polkaj-scale-types:{lib-version}` - SCALE mapping for standard Polkadot types - `io.emeraldpay.polkaj:polkaj-schnorrkel:{lib-version}` - Schnorrkel for Java +- `io.emeraldpay.polkaj:polkaj-schnorrkel-android:{lib-version-dev}` - Schnorrkel for Android - `io.emeraldpay.polkaj:polkaj-ss58:{lib-version}` - SS58 codec to encode/decode addresses and pubkeys - `io.emeraldpay.polkaj:polkaj-common-types:{lib-version}` - common types (Address, DotAmount, Hash256, etc) - `io.emeraldpay.polkaj:polkaj-json-types:{lib-version}` - JSON RPC mapping to Java classes - `io.emeraldpay.polkaj:polkaj-api-base:{lib-version}` - RPC base classes - `io.emeraldpay.polkaj:polkaj-api-http:{lib-version}` - JSON RPC HTTP client - `io.emeraldpay.polkaj:polkaj-api-ws:{lib-version}` - JSON RPC WebSocket client +- `io.emeraldpay.polkaj:polkaj-api-okhttp:{lib-version-dev}` - JSON RPC and WebSocket client using OkHttp - `io.emeraldpay.polkaj:polkaj-tx:{lib-version}` - Storage access and Extrinsics == Usage @@ -34,7 +36,7 @@ To use development SNAPSHOT versions you need to install the library into the lo .Install into local Maven ---- -gradle publishToMavenLocal +./gradlew publishToMavenLocal ---- .Using with Gradle @@ -48,7 +50,7 @@ repositories { } dependencies { - implementation 'io.emeraldpay.polkaj:polkaj-api-http:{lib-version}' + implementation 'io.emeraldpay.polkaj:polkaj-api-http:{lib-version-dev}' } ---- @@ -61,7 +63,7 @@ See link:docs/[Documentation] in `./docs` directory, and a demonstration in `./e .Show current finalized block [source,java] ---- -PolkadotHttpApi client = PolkadotApi.newBuilder() +PolkadotApi client = PolkadotApi.newBuilder() .rpcCallAdapter(JavaHttpAdapter.newBuilder().build()) .build(); Future hashFuture = client.execute( diff --git a/examples/README.adoc b/examples/README.adoc index 67c8dfc..c28c814 100644 --- a/examples/README.adoc +++ b/examples/README.adoc @@ -25,18 +25,18 @@ cd types == RPC Examples for accessing JSON RPC through HTTP or WebSockets. -The examples expect Polkadot node running and listening for RPC and WS requests on the localhost (i.e. default node options) +The examples expect Polkadot node running and listening for RPC and WS requests on the localhost (i.e. default node options). In any of the following examples you can pass the optional `--args="okhttp"` to the run command to use the Java 8 compatible OkHttp implementation of the RPC and WebSocket adapters. .Show current head ---- cd rpc -./gradlew run +./gradlew run --args="okhttp" ---- .Follow updates to the current head (use `Ctrl+C` to exit) ---- cd rpc -./gradlew run -PmainClass=FollowState +./gradlew run -PmainClass=FollowState --args="okhttp" ---- .Describe runtime @@ -45,6 +45,9 @@ cd rpc ./gradlew run -PmainClass=DescribeRuntime ---- +NOTE: Currently, only runtimes with Metadata are is supported https://github.com/paritytech/polkadot/releases/tag/v0.8.30[v0.8.30] is suitable version to test with. + + == Runtime Explorer A web-based explorer of the Runtime Metadata. @@ -78,6 +81,9 @@ cd balance NOTE: To run a development network use: `polkadot --dev` +NOTE: Currently, only runtimes with Metadata are is supported https://github.com/paritytech/polkadot/releases/tag/v0.8.30[v0.8.30] is suitable version to test with. + + .Transfer using real network (ex. Kusama) ---- cd balance diff --git a/examples/balance/build.gradle b/examples/balance/build.gradle index a41d45e..fc28dee 100644 --- a/examples/balance/build.gradle +++ b/examples/balance/build.gradle @@ -13,6 +13,7 @@ apply from: '../common.gradle' dependencies { implementation "io.emeraldpay.polkaj:polkaj-api-http:$polkajVersion" implementation "io.emeraldpay.polkaj:polkaj-api-ws:$polkajVersion" + implementation "io.emeraldpay.polkaj:polkaj-api-okhttp:$polkajVersion" implementation "io.emeraldpay.polkaj:polkaj-json-types:$polkajVersion" implementation "io.emeraldpay.polkaj:polkaj-scale-types:$polkajVersion" implementation "io.emeraldpay.polkaj:polkaj-tx:$polkajVersion" diff --git a/examples/balance/src/main/java/Balance.java b/examples/balance/src/main/java/Balance.java index fe18618..62b9285 100644 --- a/examples/balance/src/main/java/Balance.java +++ b/examples/balance/src/main/java/Balance.java @@ -1,15 +1,22 @@ import io.emeraldpay.polkaj.api.PolkadotApi; +import io.emeraldpay.polkaj.api.RpcCallAdapter; import io.emeraldpay.polkaj.apihttp.JavaHttpAdapter; +import io.emeraldpay.polkaj.apiokhttp.OkHttpRpcAdapter; import io.emeraldpay.polkaj.scaletypes.AccountInfo; import io.emeraldpay.polkaj.tx.AccountRequests; import io.emeraldpay.polkaj.types.Address; import io.emeraldpay.polkaj.types.DotAmount; import io.emeraldpay.polkaj.types.DotAmountFormatter; +import java.util.Arrays; + public class Balance { public static void main(String[] args) throws Exception { - try (PolkadotApi client = PolkadotApi.newBuilder().rpcCallAdapter(JavaHttpAdapter.newBuilder().build()).build()) { + final boolean useOkhttp = Arrays.asList(args).contains("okhttp"); + final RpcCallAdapter adapter = useOkhttp ? OkHttpRpcAdapter.newBuilder().build() : + JavaHttpAdapter.newBuilder().build(); + try (PolkadotApi client = PolkadotApi.newBuilder().rpcCallAdapter(adapter).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 dc74d0d..e34cd3a 100644 --- a/examples/balance/src/main/java/Transfer.java +++ b/examples/balance/src/main/java/Transfer.java @@ -1,4 +1,5 @@ import io.emeraldpay.polkaj.api.*; +import io.emeraldpay.polkaj.apiokhttp.OkHttpSubscriptionAdapter; import io.emeraldpay.polkaj.apiws.JavaHttpSubscriptionAdapter; import io.emeraldpay.polkaj.scale.ScaleExtract; import io.emeraldpay.polkaj.scaletypes.AccountInfo; @@ -49,9 +50,16 @@ public static void main(String[] args) throws Exception { Math.abs(random.nextLong()) % DotAmount.fromDots(0.002).getValue().longValue() ); - final JavaHttpSubscriptionAdapter adapter = JavaHttpSubscriptionAdapter.newBuilder().connectTo(api).build(); + boolean useOkhttp = true; + final SubscriptionAdapter adapter = useOkhttp ? + OkHttpSubscriptionAdapter.newBuilder().connectTo(api).build() : + JavaHttpSubscriptionAdapter.newBuilder().connectTo(api).build(); try (PolkadotApi client = PolkadotApi.newBuilder().subscriptionAdapter(adapter).build()) { - System.out.println("Connected: " + adapter.connect().get()); + if(adapter instanceof JavaHttpSubscriptionAdapter){ + //Connect call not required for OkHttp + CompletableFuture connected = ((JavaHttpSubscriptionAdapter)adapter).connect(); + System.out.println("Connected: " + connected.get()); + } // Subscribe to block heights AtomicLong height = new AtomicLong(0); diff --git a/examples/rpc/build.gradle b/examples/rpc/build.gradle index 867990e..3d09a4f 100644 --- a/examples/rpc/build.gradle +++ b/examples/rpc/build.gradle @@ -13,11 +13,12 @@ apply from: '../common.gradle' dependencies { implementation "io.emeraldpay.polkaj:polkaj-api-http:$polkajVersion" implementation "io.emeraldpay.polkaj:polkaj-api-ws:$polkajVersion" + implementation "io.emeraldpay.polkaj:polkaj-api-okhttp:$polkajVersion" implementation "io.emeraldpay.polkaj:polkaj-json-types:$polkajVersion" implementation "io.emeraldpay.polkaj:polkaj-scale-types:$polkajVersion" implementation 'commons-codec:commons-codec:1.14' } application { - mainClassName = project.hasProperty("mainClass") ? getProperty("mainClass") : "ShowState" + mainClassName = findProperty("mainClass") ?: "ShowState" } \ No newline at end of file diff --git a/examples/rpc/src/main/java/DescribeRuntime.java b/examples/rpc/src/main/java/DescribeRuntime.java index a038328..9a77a7f 100644 --- a/examples/rpc/src/main/java/DescribeRuntime.java +++ b/examples/rpc/src/main/java/DescribeRuntime.java @@ -1,10 +1,13 @@ import io.emeraldpay.polkaj.api.PolkadotApi; +import io.emeraldpay.polkaj.api.RpcCallAdapter; import io.emeraldpay.polkaj.api.StandardCommands; import io.emeraldpay.polkaj.apihttp.JavaHttpAdapter; +import io.emeraldpay.polkaj.apiokhttp.OkHttpRpcAdapter; import io.emeraldpay.polkaj.scale.ScaleExtract; import io.emeraldpay.polkaj.scaletypes.Metadata; import io.emeraldpay.polkaj.scaletypes.MetadataReader; +import java.util.Arrays; import java.util.concurrent.Future; /** @@ -13,8 +16,11 @@ public class DescribeRuntime { public static void main(String[] args) throws Exception { + final boolean useOkhttp = Arrays.stream(args).anyMatch(arg -> arg.equals("okhttp")); + final RpcCallAdapter adapter = useOkhttp ? OkHttpRpcAdapter.newBuilder().build() : + JavaHttpAdapter.newBuilder().build(); PolkadotApi api = PolkadotApi.newBuilder() - .rpcCallAdapter(JavaHttpAdapter.newBuilder().build()) + .rpcCallAdapter(adapter) .build(); Future metadataFuture = api.execute(StandardCommands.getInstance().stateMetadata()) .thenApply(ScaleExtract.fromBytesData(new MetadataReader())); diff --git a/examples/rpc/src/main/java/FollowState.java b/examples/rpc/src/main/java/FollowState.java index 877ffa1..8909575 100644 --- a/examples/rpc/src/main/java/FollowState.java +++ b/examples/rpc/src/main/java/FollowState.java @@ -1,11 +1,13 @@ import io.emeraldpay.polkaj.api.PolkadotApi; import io.emeraldpay.polkaj.api.Subscription; import io.emeraldpay.polkaj.api.SubscriptionAdapter; +import io.emeraldpay.polkaj.apiokhttp.OkHttpSubscriptionAdapter; import io.emeraldpay.polkaj.apiws.JavaHttpSubscriptionAdapter; import io.emeraldpay.polkaj.json.BlockJson; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -15,13 +17,19 @@ public class FollowState { public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException { - JavaHttpSubscriptionAdapter wsAdapter = JavaHttpSubscriptionAdapter.newBuilder().build(); + final boolean useOkhttp = Arrays.asList(args).contains("okhttp"); + final SubscriptionAdapter wsAdapter = useOkhttp ? + OkHttpSubscriptionAdapter.newBuilder().connectTo("ws://192.168.68.93:9944").build() : + JavaHttpSubscriptionAdapter.newBuilder().build(); + PolkadotApi api = PolkadotApi.newBuilder() .subscriptionAdapter(wsAdapter) .build(); // IMPORTANT! connect to the node as the first step before making calls or subscriptions. - wsAdapter.connect().get(5, TimeUnit.SECONDS); + // OkHttpSubscriptionAdapter handles this for you + if(wsAdapter instanceof JavaHttpSubscriptionAdapter) + ((JavaHttpSubscriptionAdapter)wsAdapter).connect().get(5, TimeUnit.SECONDS); Future> hashFuture = api.subscribe(SubscriptionAdapter.subscriptions().newHeads()); diff --git a/examples/rpc/src/main/java/ShowState.java b/examples/rpc/src/main/java/ShowState.java index 1a72fef..014a386 100644 --- a/examples/rpc/src/main/java/ShowState.java +++ b/examples/rpc/src/main/java/ShowState.java @@ -1,20 +1,26 @@ import io.emeraldpay.polkaj.api.PolkadotApi; import io.emeraldpay.polkaj.api.PolkadotMethod; import io.emeraldpay.polkaj.api.RpcCall; +import io.emeraldpay.polkaj.api.RpcCallAdapter; import io.emeraldpay.polkaj.apihttp.JavaHttpAdapter; +import io.emeraldpay.polkaj.apiokhttp.OkHttpRpcAdapter; import io.emeraldpay.polkaj.json.BlockResponseJson; import io.emeraldpay.polkaj.json.RuntimeVersionJson; import io.emeraldpay.polkaj.json.SystemHealthJson; import io.emeraldpay.polkaj.types.Hash256; +import java.util.Arrays; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; public class ShowState { public static void main(String[] args) throws Exception { + final boolean useOkhttp = Arrays.asList(args).contains("okhttp"); + final RpcCallAdapter adapter = useOkhttp ? OkHttpRpcAdapter.newBuilder().build() : + JavaHttpAdapter.newBuilder().build(); PolkadotApi api = PolkadotApi.newBuilder() - .rpcCallAdapter(JavaHttpAdapter.newBuilder().build()) + .rpcCallAdapter(adapter) .build(); Future hashFuture = api.execute( @@ -58,17 +64,5 @@ public static void main(String[] args) throws Exception { 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; -// } } diff --git a/polkaj-api-http/README.adoc b/polkaj-api-http/README.adoc index 5d533de..ad5874f 100644 --- a/polkaj-api-http/README.adoc +++ b/polkaj-api-http/README.adoc @@ -5,14 +5,14 @@ HTTP client to JSON RPC server provided by a Polkadot node. .Example [source, java] ---- -import io.emeraldpay.polkaj.clientrpc.PolkadotRpcClient; +import io.emeraldpay.polkaj.apihttp.JavaHttpAdapter; import io.emeraldpay.polkaj.types.Hash256; import io.emeraldpay.polkaj.json.BlockResponseJson; -PolkadotRpcClient client = PolkadotRpcClient.newBuilder().build(); +JavaHttpAdapter client = JavaHttpAdapter.newBuilder().build(); Future hash = client.execute(Hash256.class, "chain_getFinalisedHead"); System.out.println("Current head: " + hash.get()); -Future block = client.execute(BlockResponseJson.class, "chain_getBlock", hash.get()); +Future block = client.produceRpcFuture(BlockResponseJson.class, "chain_getBlock", hash.get()); System.out.println("Current height: " + block.get().getBlock().getHeader().getNumber()); ---- \ No newline at end of file diff --git a/polkaj-api-okhttp/README.adoc b/polkaj-api-okhttp/README.adoc new file mode 100644 index 0000000..85f5fa4 --- /dev/null +++ b/polkaj-api-okhttp/README.adoc @@ -0,0 +1,32 @@ += OkHttp adapters + +HTTP client to JSON RPC server provided by a Polkadot node. + +.RPC Example +[source, java] +---- +import io.emeraldpay.polkaj.apiokhttp.OkHttpRpcAdapter; +import io.emeraldpay.polkaj.types.Hash256; +import io.emeraldpay.polkaj.json.BlockResponseJson; + +OkHttpRpcAdapter client = OkHttpRpcAdapter.newBuilder().build(); +Future hash = client.produceRpcFuture(Hash256.class, "chain_getFinalisedHead"); +System.out.println("Current head: " + hash.get()); + +Future block = client.produceRpcFuture(BlockResponseJson.class, "chain_getBlock", hash.get()); +System.out.println("Current height: " + block.get().getBlock().getHeader().getNumber()); +---- + +.Subscription Websoocket Example +[source, java] +---- +OkHttpSubscriptionAdapter client = OkHttpSubscriptionAdapter.newBuilder().build(); +AtomicLong height = new AtomicLong(0); +CompletableFuture waitForBlocks = new CompletableFuture<>(); +client.subscribe( + StandardSubscriptions.getInstance().newHeads() +).get().handler((event) -> { + long current = event.getResult().getNumber(); + System.out.println("Current height: " + current); +}); +---- diff --git a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt index 17dbad7..a059941 100644 --- a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt +++ b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpRpcAdapter.kt @@ -106,8 +106,22 @@ class OkHttpRpcAdapter private constructor( } } + /** + * Server address URL. + * By default, it will be set to "ws://127.0.0.1:9944" + * + * @param target URL + * @return builder + */ fun target(target: String) = apply { this.target = target.toHttpUrl() } + /** + * Setup Basic Auth for RPC calls + * + * @param username username + * @param password password + * @return builder + */ fun basicAuth(username: String, password: String) : Builder{ return apply { val combine = "$username:$password".toByteArray() @@ -115,11 +129,47 @@ class OkHttpRpcAdapter private constructor( } } + /** + * Provide a custom OkHttpClient configured + * + * @param client OkHttpClient + * @return builder + */ fun client(client : OkHttpClient) = apply { this.client = client } + + /** + * CoroutineScope for requests and subscription. + * By default, a new Scope will be created. + */ fun scope(scope : CoroutineScope) = apply { this.scope = scope } + + /** + * Provide a custom RpcCoder for rpc serialization. + * + * @param rpcCoder rpcCoder + * @return builder + */ fun rpcCoder(rpcCoder : RpcCoder) = apply { this.rpcCoder = rpcCoder } + + /** + * Provide custom cleanup method. + * By default, it will cancel the [scope] and shutdown the [client] executorService + * + * @param block to be called on close. + * @return builder + */ fun onClose(block : () -> Unit ) = apply { onClose = block } + + /** + * Provide a custom timeout + * By default, it is 1 minute. + */ fun timeout(timeout : Duration) = apply { client = client.newBuilder().callTimeout(timeout).build() } + + /** + * Apply configuration and build a new adapter + * @return new instance of [OkHttpRpcAdapter] + */ fun build() : OkHttpRpcAdapter = OkHttpRpcAdapter(target, basicAuth, client, scope, rpcCoder, onClose) } diff --git a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt index 7998b45..9e9e3cb 100644 --- a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt +++ b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt @@ -14,13 +14,15 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.future.asCompletableFuture import okhttp3.* import java.time.Duration -import java.util.* import java.util.concurrent.CompletableFuture import java.util.function.Consumer +/** + * OkHttp 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. + */ class OkHttpSubscriptionAdapter private constructor( private val target : String, - private val basicAuth : String?, private val client : OkHttpClient, private val scope : CoroutineScope, private val rpcCoder: RpcCoder, @@ -157,7 +159,6 @@ class OkHttpSubscriptionAdapter private constructor( url(target) header("User-Agent", "PolkaJ/OkHttp/0.5") header("Content-Type", APPLICATION_JSON) - if(basicAuth != null) header("Authorization", basicAuth) }.build() return callbackFlow { @@ -292,20 +293,56 @@ class OkHttpSubscriptionAdapter private constructor( } } - fun target(target: String) = apply { this.target = target } - - fun basicAuth(username: String, password: String) : Builder{ - return apply { - val combine = "$username:$password".toByteArray() - basicAuth = "Basic ${Base64.getEncoder().encodeToString(combine)}" - } - } - + /** + * Server address URL. + * By default, it will be set to "ws://127.0.0.1:9944" + * + * @param target URL + * @return builder + */ + fun connectTo(target: String) = apply { this.target = target } + + /** + * Provide a custom OkHttpClient configured + * + * @param client OkHttpClient + * @return builder + */ fun client(client : OkHttpClient) = apply { this.client = client } + + /** + * CoroutineScope for requests and subscription. + * By default, a new Scope will be created. + */ fun scope(scope : CoroutineScope) = apply { this.scope = scope } + + /** + * Provide a custom RpcCoder for rpc serialization. + * + * @param rpcCoder rpcCoder + * @return builder + */ fun rpcCoder(rpcCoder : RpcCoder) = apply { this.rpcCoder = rpcCoder } + + /** + * Provide custom cleanup method. + * By default, it will cancel the [scope] and shutdown the [client] executorService + * + * @param block to be called on close. + * @return builder + */ fun onClose(block : () -> Unit ) = apply { onClose = block } - fun timeout(timeout : Duration) = apply { client = client.newBuilder().callTimeout(timeout).build() } - fun build() : OkHttpSubscriptionAdapter = OkHttpSubscriptionAdapter(target, basicAuth, client, scope, rpcCoder, onClose) + + /** + * Provide a custom timeout + * By default, it is 1 minute. + */ + fun timeout(timeout : Duration) = apply { client = client.newBuilder().callTimeout(timeout).build() } + + /** + * Apply configuration and build a new adapter + * @return new instance of [OkHttpSubscriptionAdapter] + */ + fun build() : OkHttpSubscriptionAdapter = OkHttpSubscriptionAdapter(target, client, scope, rpcCoder, onClose) } } \ No newline at end of file diff --git a/polkaj-api-okhttp/src/test/groovy/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapterSpec.groovy b/polkaj-api-okhttp/src/test/groovy/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapterSpec.groovy index 5ad5b66..ff8a728 100644 --- a/polkaj-api-okhttp/src/test/groovy/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapterSpec.groovy +++ b/polkaj-api-okhttp/src/test/groovy/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapterSpec.groovy @@ -2,7 +2,6 @@ package io.emeraldpay.polkaj.apiokhttp import io.emeraldpay.polkaj.api.SubscriptionAdapter import io.emeraldpay.polkaj.api.SubscriptionAdapterSpec -import io.emeraldpay.polkaj.apiokhttp.OkHttpSubscriptionAdapter import java.time.Duration @@ -10,7 +9,7 @@ class OkHttpSubscriptionAdapterSpec extends SubscriptionAdapterSpec { @Override SubscriptionAdapter provideAdapter(String connectTo) { - return OkHttpSubscriptionAdapter.newBuilder().target(connectTo).timeout(Duration.ofSeconds(15)).build() + return OkHttpSubscriptionAdapter.newBuilder().connectTo(connectTo).timeout(Duration.ofSeconds(15)).build() } } \ No newline at end of file diff --git a/polkaj-api-okhttp/src/test/kotlin/io/emeraldpay/polkaj/OkHttpSubscriptionAdapterTests.kt b/polkaj-api-okhttp/src/test/kotlin/io/emeraldpay/polkaj/OkHttpSubscriptionAdapterTests.kt index d9aad69..f611c0f 100644 --- a/polkaj-api-okhttp/src/test/kotlin/io/emeraldpay/polkaj/OkHttpSubscriptionAdapterTests.kt +++ b/polkaj-api-okhttp/src/test/kotlin/io/emeraldpay/polkaj/OkHttpSubscriptionAdapterTests.kt @@ -42,9 +42,8 @@ class OkHttpSubscriptionAdapterTests { private val subscriptionAdapter = OkHttpSubscriptionAdapter.Builder { timeout(Duration.ofSeconds(1)) - target(target) + connectTo(target) scope(scope) - basicAuth("alice", "secret") rpcCoder(rpcCoder) client(client) onClose(onClose) @@ -101,14 +100,6 @@ class OkHttpSubscriptionAdapterTests { } } - @Test - fun `client uses basic auth`(){ - mockResponse() - val call = RpcCall.create(String::class.java, "chain_getFinalisedHead") - subscriptionAdapter.produceRpcFuture(call).get() - assertEquals("Basic YWxpY2U6c2VjcmV0", request.captured.headers["Authorization"]) - } - @Test fun `socket failure given to pending request`(){ val exception = Exception() diff --git a/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/JavaHttpSubscriptionAdapter.java b/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/JavaHttpSubscriptionAdapter.java index 9760e8e..6c26b99 100644 --- a/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/JavaHttpSubscriptionAdapter.java +++ b/polkaj-api-ws/src/main/java/io/emeraldpay/polkaj/apiws/JavaHttpSubscriptionAdapter.java @@ -354,9 +354,9 @@ private void initDefaults() { } /** - * Apply configuration and build client + * Apply configuration and build adapter * - * @return new instance of PolkadotRpcClient + * @return new instance of JavaHttpSubscriptionAdapter */ public JavaHttpSubscriptionAdapter build() { initDefaults(); diff --git a/polkaj-schnorrkel-android/README.adoc b/polkaj-schnorrkel-android/README.adoc new file mode 100644 index 0000000..8376e0b --- /dev/null +++ b/polkaj-schnorrkel-android/README.adoc @@ -0,0 +1,9 @@ +== Schnorrkel / Ristretto x25519 (aka sr25519) + +Android Library Java wrapper around Rust implementation of the Schnorrkel / Ristretto x25519 + +Implementation is based on WASM code: + +- https://github.com/polkadot-js/wasm/ + +Rust libs are built using https://docs.rs/crate/cargo-ndk/2.4.1[cargo-ndk] see link:../cargo_ndk_prep.sh[cargo_ndk_prep.sh] \ No newline at end of file From 68ed443904b345d8da56b5216f71955757bd4686 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Tue, 5 Oct 2021 01:10:58 -0500 Subject: [PATCH 37/38] fix race condition could occur when causing first websocket response to not be read --- .../apiokhttp/OkHttpSubscriptionAdapter.kt | 65 ++++++++----------- .../polkaj/OkHttpSubscriptionAdapterTests.kt | 29 +++++++++ 2 files changed, 57 insertions(+), 37 deletions(-) diff --git a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt index 9e9e3cb..52f7594 100644 --- a/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt +++ b/polkaj-api-okhttp/src/main/kotlin/io/emeraldpay/polkaj/apiokhttp/OkHttpSubscriptionAdapter.kt @@ -38,7 +38,8 @@ class OkHttpSubscriptionAdapter private constructor( val socketState : SocketState = SocketState.Idle, val rpcCalls : List> = emptyList(), val subscriptionCalls : List> = emptyList(), - val curSocketJob : Job? = null + val curSocketJob : Job? = null, + val startingSub : Set> = setOf() ) private sealed class SocketState { @@ -85,7 +86,7 @@ class OkHttpSubscriptionAdapter private constructor( } private val _state = MutableStateFlow(State()) - private val _messages = MutableSharedFlow(1) + private val _messages = MutableSharedFlow(0) private val curState = _state.asStateFlow() private val messages = _messages.asSharedFlow() @@ -98,7 +99,15 @@ class OkHttpSubscriptionAdapter private constructor( curState.value.rpcCalls.firstOrNull { it.id == id }?.call?.getResultType(rpcCoder.objectMapper.typeFactory) } val subMapping = { id : String -> - curState.value.subscriptionCalls.firstOrNull { it.id == id }?.call?.getResultType(rpcCoder.objectMapper.typeFactory) + runBlocking { + curState.transformWhile { state -> + state.subscriptionCalls.firstOrNull{ it.id == id}?.call?.getResultType(rpcCoder.objectMapper.typeFactory)?.let { + emit(it) + } + state.startingSub.isNotEmpty() + }.first() + } + } decodeResponse = DecodeResponse(rpcCoder.objectMapper, rpcMapping, subMapping) } @@ -126,13 +135,17 @@ class OkHttpSubscriptionAdapter private constructor( } if(payload != null) { val rpc = RpcDeferred(id, call, result) - _state += rpc + _state.update { + it.copy(rpcCalls = it.rpcCalls + rpc) + } curState.socket().send(String(payload)) rpcEvents.first { it.id == id }.let { if(it.error != null) result.completeExceptionally(RpcException(it.error.code, it.error.message, it.error.data)) else result.complete(it.result as T) }.also { - _state -= rpc + _state.update { + it.copy(rpcCalls = it.rpcCalls - rpc) + } } } } @@ -143,13 +156,18 @@ class OkHttpSubscriptionAdapter private constructor( @Suppress("UNCHECKED_CAST") override fun subscribe(call: SubscribeCall): CompletableFuture> { val start = RpcCall.create(String::class.java, call.method, *call.params) + _state.update { it.copy(startingSub = it.startingSub + start) } return produceRpcFuture(start).thenApply { id -> val events = subscriptionEvents.filter { it.id == id }.map { Subscription.Event(it.method, it.value as T) } - FlowSubscription(id, call, scope, events) { + FlowSubscription(id, call, scope, events) { sub -> produceRpcFuture(RpcCall.create(Boolean::class.java, call.unsubscribe, id)) - _state -= it - }.also { - _state += it + _state.update { + it.copy(subscriptionCalls = it.subscriptionCalls - sub) + } + }.also { sub -> + _state.update { + it.copy(subscriptionCalls = it.subscriptionCalls + sub, startingSub = it.startingSub - start) + } } } } @@ -199,12 +217,10 @@ class OkHttpSubscriptionAdapter private constructor( }.buffer(Channel.UNLIMITED).onEach { when(it){ - SocketState.Closed -> _state += SocketState.Idle - SocketState.Closing -> _state += SocketState.Idle is SocketState.Connected -> _state += it SocketState.Connecting -> _state += it is SocketState.Failed -> handleSocketException(it.throwable) - SocketState.Idle -> _state += it + else -> _state += SocketState.Idle } } } @@ -239,39 +255,14 @@ class OkHttpSubscriptionAdapter private constructor( if(it is SocketState.Connected) emit(it.webSocket) }.first() - private operator fun MutableStateFlow.plusAssign(rpcDeferred : RpcDeferred) { - _state.update { - it.copy(rpcCalls = it.rpcCalls + rpcDeferred) - } - } - - private operator fun MutableStateFlow.minusAssign(rpcDeferred : RpcDeferred) { - _state.update { - it.copy(rpcCalls = it.rpcCalls - rpcDeferred) - } - } - private operator fun MutableStateFlow.plusAssign(socketState: SocketState) { _state.update { it.copy(socketState = socketState) } } - private operator fun MutableStateFlow.plusAssign(subscription : FlowSubscription) { - _state.update { - it.copy(subscriptionCalls = it.subscriptionCalls + subscription) - } - } - - private operator fun MutableStateFlow.minusAssign(subscription : FlowSubscription) { - _state.update { - it.copy(subscriptionCalls = it.subscriptionCalls - subscription) - } - } - class Builder private constructor( private var target : String = "ws://127.0.0.1:9944", - private var basicAuth : String? = null, private var client : OkHttpClient = OkHttpClient.Builder().apply { callTimeout(Duration.ofMinutes(1)) followRedirects(false) diff --git a/polkaj-api-okhttp/src/test/kotlin/io/emeraldpay/polkaj/OkHttpSubscriptionAdapterTests.kt b/polkaj-api-okhttp/src/test/kotlin/io/emeraldpay/polkaj/OkHttpSubscriptionAdapterTests.kt index f611c0f..80ae8e3 100644 --- a/polkaj-api-okhttp/src/test/kotlin/io/emeraldpay/polkaj/OkHttpSubscriptionAdapterTests.kt +++ b/polkaj-api-okhttp/src/test/kotlin/io/emeraldpay/polkaj/OkHttpSubscriptionAdapterTests.kt @@ -3,10 +3,15 @@ package io.emeraldpay.polkaj import com.fasterxml.jackson.databind.ObjectMapper import io.emeraldpay.polkaj.api.RpcCall import io.emeraldpay.polkaj.api.RpcCoder +import io.emeraldpay.polkaj.api.StandardSubscriptions +import io.emeraldpay.polkaj.api.SubscribeCall import io.emeraldpay.polkaj.apiokhttp.OkHttpSubscriptionAdapter import io.emeraldpay.polkaj.json.jackson.PolkadotModule import io.mockk.* +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.withTimeout import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.WebSocket @@ -17,6 +22,8 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.time.Duration import java.util.concurrent.ExecutionException +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine import kotlin.test.assertEquals class OkHttpSubscriptionAdapterTests { @@ -120,6 +127,28 @@ class OkHttpSubscriptionAdapterTests { assertEquals(exception, caught.cause?.cause) } + @Test + fun `handles subscribe id race condition`() : Unit = runBlocking { + val subResponse = "{\"jsonrpc\":\"2.0\",\"result\":\"EsqruyKPnZvPZ6fr\",\"id\":0}" + val block = " {\"jsonrpc\":\"2.0\",\"method\":\"chain_newHead\",\"params\":{\"result\":{\"digest\":{\"logs\":[]},\"extrinsicsRoot\":\"0x9869230c3cc05051ce9afef4458d2515fb2141bfd3bdcd88292f41e17ea00ae7\",\"number\":\"0x1d878c\",\"parentHash\":\"0xbe9110f6da6a19ac645a27472e459dcca6eaf4ee4b0b12700ca5d566eea9a638\",\"stateRoot\":\"0x57059722d680b591a469937449df772b95625d4230b39a0a7d855e16d597f168\"},\"subscription\":\"EsqruyKPnZvPZ6fr\"}}\n" + val sub = StandardSubscriptions.getInstance().newHeads() + every { webSocket.send(any()) } answers { + val l = listener.captured + //simulate subscribe response faster than id for type placed in table + l.onMessage(webSocket, block) //will not be able to parse block until we give id on next line + l.onMessage(webSocket, subResponse) // reply with id + true + } + + withTimeout(500){ + suspendCoroutine { cont -> + subscriptionAdapter.subscribe(sub).get().handler { + cont.resume(Unit) + } + } + } + } + private fun mockResponse(){ every { webSocket.send(any()) } answers { listener.captured.onMessage(webSocket, finalizedHeadResponse) From 546b38e3af78e7a01de7f375404b50f4e80e35f0 Mon Sep 17 00:00:00 2001 From: Nathan Schwermann Date: Tue, 5 Oct 2021 01:28:53 -0500 Subject: [PATCH 38/38] undo always run tests --- .github/workflows/test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c47ae02..f477647 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -3,8 +3,8 @@ name: Tests on: # if pushed directly to the master push: -# branches: -# - master + branches: + - master # on a pull request pull_request: branches: