Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions api/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
dependencies {
implementation project(':rpc')
implementation project(':rpc:rpc-core')
implementation project(':rpc:rpc-sections')
implementation project(':transport')
implementation project(':pallet')
implementation project(':scale')
implementation project(':types')
implementation project(':rpc:rpc-types')
implementation project(':storage')

testImplementation project(':tests')

testImplementation 'org.testcontainers:testcontainers:1.16.3'
testImplementation 'org.testcontainers:junit-jupiter:1.16.3'

testAnnotationProcessor project(':pallet:pallet-codegen')
}
26 changes: 26 additions & 0 deletions api/src/main/java/com/strategyobject/substrateclient/api/Api.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,33 @@
package com.strategyobject.substrateclient.api;

import com.strategyobject.substrateclient.pallet.GeneratedPalletResolver;
import com.strategyobject.substrateclient.rpc.Rpc;
import com.strategyobject.substrateclient.rpc.RpcImpl;
import com.strategyobject.substrateclient.transport.ProviderInterface;
import lombok.val;

/**
* Provides the ability to query a node and interact with the Polkadot or Substrate chains.
* It allows interacting with blockchain in various ways: using RPC's queries directly or
* accessing Pallets and its APIs, such as storages, transactions, etc.
*/
public interface Api {
static DefaultApi with(ProviderInterface provider) {
val rpc = RpcImpl.with(provider);

return DefaultApi.with(rpc, GeneratedPalletResolver.with(rpc));
}

/**
* @return the instance that provides a proper API for querying the RPC's methods.
*/
Rpc rpc();

/**
* Resolves the instance of a pallet by its definition.
* @param clazz the class of the pallet
* @param <T> the type of the pallet
* @return appropriate instance of the pallet
*/
<T> T pallet(Class<T> clazz);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.strategyobject.substrateclient.api;

import com.strategyobject.substrateclient.pallet.PalletResolver;
import com.strategyobject.substrateclient.rpc.Rpc;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@RequiredArgsConstructor(staticName = "with")
public class DefaultApi implements Api, AutoCloseable {
private final @NonNull Rpc rpc;
private final @NonNull PalletResolver palletResolver;
private final Map<Class<?>, Object> palletCache = new ConcurrentHashMap<>();

@Override
public Rpc rpc() {
return rpc;
}

@Override
public <T> T pallet(@NonNull Class<T> clazz) {
return clazz.cast(palletCache
.computeIfAbsent(clazz, palletResolver::resolve));
}

@Override
public void close() throws Exception {
if (rpc instanceof AutoCloseable) {
((AutoCloseable) rpc).close();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.strategyobject.substrateclient.api;

import com.strategyobject.substrateclient.tests.containers.SubstrateVersion;
import com.strategyobject.substrateclient.tests.containers.TestSubstrateContainer;
import com.strategyobject.substrateclient.transport.ws.WsProvider;
import lombok.val;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.math.BigInteger;
import java.util.concurrent.TimeUnit;

import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@Testcontainers
public class ApiTests {
private static final int WAIT_TIMEOUT = 1000;

@Container
private final TestSubstrateContainer substrate = new TestSubstrateContainer(SubstrateVersion.V3_0_0);

@Test
public void getSystemPalletAndCall() throws Exception { // TODO move the test out of the project
val wsProvider = WsProvider.builder()
.setEndpoint(substrate.getWsAddress())
.build();
wsProvider.connect().get(WAIT_TIMEOUT, TimeUnit.SECONDS);

try (val api = Api.with(wsProvider)) {
val systemPallet = api.pallet(SystemPallet.class);
val blockHash = systemPallet
.blockHash()
.get(0)
.get(WAIT_TIMEOUT, TimeUnit.SECONDS);

assertNotNull(blockHash);
assertNotEquals(BigInteger.ZERO, new BigInteger(blockHash.getData()));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.strategyobject.substrateclient.api;

import com.strategyobject.substrateclient.pallet.annotations.Pallet;
import com.strategyobject.substrateclient.pallet.annotations.Storage;
import com.strategyobject.substrateclient.pallet.annotations.StorageHasher;
import com.strategyobject.substrateclient.pallet.annotations.StorageKey;
import com.strategyobject.substrateclient.rpc.types.BlockHash;
import com.strategyobject.substrateclient.scale.annotations.Scale;
import com.strategyobject.substrateclient.storage.StorageNMap;

@Pallet("System")
public interface SystemPallet {
@Storage(
value = "BlockHash",
keys = {
@StorageKey(
type = @Scale(Integer.class),
hasher = StorageHasher.TwoX64Concat
)
})
StorageNMap<BlockHash> blockHash();
}
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {

allprojects {
group = 'com.strategyobject.substrateclient'
version = '0.0.4-SNAPSHOT'
version = '0.1.0-SNAPSHOT'

repositories {
mavenLocal()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ public String getPackageName(@NonNull TypeElement classElement) {
return elementUtils.getPackageOf(classElement).getQualifiedName().toString();
}

public boolean isSubtypeOf(@NonNull TypeMirror candidate, @NonNull TypeMirror supertype) {
public boolean isAssignable(@NonNull TypeMirror candidate, @NonNull TypeMirror supertype) {
return typeUtils.isAssignable(candidate, supertype);
}

public boolean isSubtype(@NonNull TypeMirror candidate, @NonNull TypeMirror supertype) {
return typeUtils.isSubtype(candidate, supertype);
}

public boolean isGeneric(@NonNull TypeMirror type) {
return ((TypeElement) typeUtils.asElement(type))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,29 @@ public T traverse(@NonNull TypeMirror type, @NonNull TypeTraverser.TypeTreeNode
.toArray(x -> (T[]) Array.newInstance(clazz, typeArguments.size())));
}

@SuppressWarnings({"unchecked"})
public T traverse(@NonNull TypeTraverser.TypeTreeNode typeOverride) {
if (typeOverride.type.getKind().isPrimitive()) {
return whenPrimitiveType((PrimitiveType) typeOverride.type, typeOverride.type);
}

if (!(typeOverride.type instanceof DeclaredType)) {
throw new IllegalArgumentException("Type is not supported: " + typeOverride.type);
}

val declaredType = (DeclaredType) typeOverride.type;
if (typeOverride.children.size() == 0) {
return whenNonGenericType(declaredType, typeOverride.type);
}

return whenGenericType(
declaredType,
typeOverride.type,
typeOverride.children.stream()
.map(this::traverse)
.toArray(x -> (T[]) Array.newInstance(clazz, typeOverride.children.size())));
}

private List<? extends TypeMirror> getTypeArgumentsOrDefault(DeclaredType declaredType, TypeMirror override) {
return (doTraverseArguments(declaredType, override) ?
declaredType.getTypeArguments() :
Expand All @@ -125,6 +148,7 @@ private boolean typeIsOverriddenByNonGeneric(int typeOverrideSize) {


public static class TypeTreeNode {
@Getter
private final TypeMirror type;
private final List<TypeTreeNode> children;

Expand Down
4 changes: 4 additions & 0 deletions pallet/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dependencies {
implementation project(':scale')
implementation project(':rpc')
}
16 changes: 16 additions & 0 deletions pallet/pallet-codegen/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
dependencies {
implementation project(':common')
implementation project(':rpc')
implementation project(':types')
implementation project(':scale')
implementation project(':scale:scale-codegen')
implementation project(':storage')
implementation project(':pallet')

implementation 'com.squareup:javapoet:1.13.0'

compileOnly 'com.google.auto.service:auto-service-annotations:1.0.1'
annotationProcessor 'com.google.auto.service:auto-service:1.0.1'

testImplementation 'com.google.testing.compile:compile-testing:0.19'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.strategyobject.substrateclient.pallet;

import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import com.strategyobject.substrateclient.common.codegen.ProcessingException;
import com.strategyobject.substrateclient.common.codegen.ProcessorContext;
import lombok.NonNull;
import lombok.var;

import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import java.util.List;

class CompoundMethodProcessor extends PalletMethodProcessor {
private final List<PalletMethodProcessor> processors;

public CompoundMethodProcessor(TypeElement typeElement, List<PalletMethodProcessor> processors) {
super(typeElement);
this.processors = processors;
}

@Override
void process(@NonNull String palletName, @NonNull ExecutableElement method, TypeSpec.@NonNull Builder typeSpecBuilder, MethodSpec.Builder constructorBuilder, @NonNull ProcessorContext context) throws ProcessingException {
for (var processor : processors) {
processor.process(palletName,
method,
typeSpecBuilder,
constructorBuilder,
context);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.strategyobject.substrateclient.pallet;

class Constants {
static final String RPC = "rpc";
static final String CLASS_NAME_TEMPLATE = "%sImpl";
static final String SCALE_READER_REGISTRY = "scaleReaderRegistry";
static final String SCALE_WRITER_REGISTRY = "scaleWriterRegistry";
static final String STORAGE_FACTORY_METHOD = "with";
static final String STORAGE_KEY_PROVIDER_FACTORY_METHOD = "of";
static final String STORAGE_KEY_PROVIDER_ADD_HASHERS = "use";
static final String KEY_HASHER_FACTORY_METHOD = "with";
static final String BLAKE_2_128_CONCAT_INSTANCE = "getInstance";
static final String TWO_X64_CONCAT_INSTANCE = "getInstance";
static final String IDENTITY_INSTANCE = "getInstance";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.strategyobject.substrateclient.pallet;

import com.google.common.base.Strings;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.strategyobject.substrateclient.common.codegen.ProcessingException;
import com.strategyobject.substrateclient.common.codegen.ProcessorContext;
import com.strategyobject.substrateclient.pallet.annotations.Pallet;
import com.strategyobject.substrateclient.rpc.Rpc;
import lombok.val;

import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import java.io.IOException;

import static com.strategyobject.substrateclient.pallet.Constants.CLASS_NAME_TEMPLATE;
import static com.strategyobject.substrateclient.pallet.Constants.RPC;

public class PalletAnnotatedInterface {
private final TypeElement interfaceElement;
private final String name;
private final PalletMethodProcessor methodProcessor;

public PalletAnnotatedInterface(TypeElement interfaceElement, PalletMethodProcessor methodProcessor) throws ProcessingException {
this.interfaceElement = interfaceElement;
val annotation = interfaceElement.getAnnotation(Pallet.class);

if (!interfaceElement.getModifiers().contains(Modifier.PUBLIC)) {
throw new ProcessingException(
interfaceElement,
"`%s` is not public. That is not allowed.",
interfaceElement.getQualifiedName().toString());
}

if (Strings.isNullOrEmpty(name = annotation.value())) {
throw new ProcessingException(
interfaceElement,
"`@%s` of `%s` contains null or empty `value`.",
annotation.getClass().getSimpleName(),
interfaceElement.getQualifiedName().toString());
}

this.methodProcessor = methodProcessor;
}

public void generateClass(ProcessorContext context) throws ProcessingException, IOException {
val interfaceName = interfaceElement.getSimpleName().toString();
val className = String.format(CLASS_NAME_TEMPLATE, interfaceName);
val packageName = context.getPackageName(interfaceElement);

val typeSpecBuilder = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addSuperinterface(TypeName.get(interfaceElement.asType()))
.addField(Rpc.class, RPC, Modifier.FINAL, Modifier.PRIVATE);

val constructorBuilder = createConstructorBuilder();

for (val method : interfaceElement.getEnclosedElements()) {
this.methodProcessor.process(name,
(ExecutableElement) method,
typeSpecBuilder,
constructorBuilder,
context);
}

typeSpecBuilder.addMethod(constructorBuilder.build());

JavaFile.builder(packageName, typeSpecBuilder.build()).build().writeTo(context.getFiler());
}

private MethodSpec.Builder createConstructorBuilder() {
return MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(Rpc.class, RPC)
.beginControlFlow("if ($L == null)", RPC)
.addStatement("throw new $T(\"$L can't be null.\")", IllegalArgumentException.class, RPC)
.endControlFlow()
.addStatement("this.$1L = $1L", RPC);
}
}
Loading