Overview

This tutorial explains how to create a SingularityNET service client in Java language. The content of the tutorial assumes reader is familiar with Java programming language and Maven or Gradle project management system. One can find the full code of the tutorial application in Java SDK repository. SingularityNET Java SDK API documentation is located at Jitpack. In order to complete the tutorial one should have JDK 8 or greater, Maven or Gradle and Docker installed on the local machine.

Setup environment

We are going to use a local SingularityNET environment to run the tutorial application. It simplifies the things and allow concentrating on the code only. Nevetherless sometimes it leads to the additonal configuration parameters which are described separately.

First start the environment docker using command below.

docker run -p 5002:5002 -p 8545:8545 -p 7000:7000 \
    -ti singularitynet/snet-local-env:5.0.1

This environment contains local Ethereum, local IPFS and local Example Service instances. In order to use them we need propagating three network ports from the environment. IPFS port 5002, Ethereum JSON RPC port 8545 and SingularityNET daemon port 7000.

Setup project

Three things are required to setup the project correctly:

First step is trivial, second step is done using plugin provided by SingularityNET SDK, third step is done using gRPC plugin. Please read next paragraph before moving further because it explains SingularityNET plugin parameters. Then move to one of the sections below depending on project management system you are using.

In order to use a service one needs adding the service API as a part of the project. API of the service is kept in the platform Registry. SingularityNET SDK provides Maven and Gradle plugins which automate API downloading and unpacking.

Plugins input number of parameters to get the API:

There are couple of additional parameters in the code below. ipfsRpcEndpoint and registryAddress are optional we should specify them properly when we are using a custom SingularityNET environment.

Next two sections explain how to setup Maven and Gradle projects.

Maven

Generate new Maven project using command:

mvn archetype:generate -DgroupId=io.singularitynet.sdk.tutorial \
	-DartifactId=example-service-client \
	-DarchetypeArtifactId=maven-archetype-quickstart \
	-DarchetypeVersion=1.4 \
	-DinteractiveMode=false

Go to the project directory example-service-client and edit pom.xml file. Set value of the maven.compiler.source and maven.compiler.target properties to 1.8. Add Jitpack repository to the project section of the Maven pom.xml.

<repositories>
  <repository>
    <id>jitpack.io</id>
    <url>https://jitpack.io</url>
  </repository>
</repositories>
<pluginRepositories>
  <pluginRepository>
    <id>jitpack.io</id>
    <url>https://jitpack.io</url>
  </pluginRepository>
</pluginRepositories>

Add Java SDK artifact as a Maven compilation time dependency (dependencies section of the pom.xml).

<dependency>
  <groupId>com.github.singnet.snet-sdk-java</groupId>
  <artifactId>snet-sdk-java</artifactId>
  <version>0.4.0</version>
</dependency>

Add new <plugins></plugins> section inside <build></build> section and put the plugins declarations below inside.

Use snet-maven-sdk-plugin to download and unpack the API of the service. Add the following code under plugins section of the Maven pom.xml.

<plugin>
  <groupId>com.github.singnet.snet-sdk-java</groupId>
  <artifactId>snet-sdk-maven-plugin</artifactId>
  <version>0.4.0</version>
  <executions>
    <execution>

      <configuration>
        <orgId>example-org</orgId>
        <serviceId>example-service</serviceId>
        <outputDir>${project.build.directory}/proto</outputDir>
        <javaPackage>io.singularitynet.service.exampleservice</javaPackage>
        <ethereumJsonRpcEndpoint>http://localhost:8545</ethereumJsonRpcEndpoint>
        <!-- for the custom environment only -->
        <ipfsRpcEndpoint>http://localhost:5002</ipfsRpcEndpoint>
        <registryAddress>0x4e74fefa82e83e0964f0d9f53c68e03f7298a8b2</registryAddress>
      </configuration>

      <goals>
        <goal>get</goal>
      </goals>

    </execution>
  </executions>
</plugin>

Use Protobuf and gRPC Maven plugins to compile the API of the service.

<project>
  <build>
    ...
    <extensions>
      <extension>
        <groupId>kr.motd.maven</groupId>
        <artifactId>os-maven-plugin</artifactId>
        <version>1.6.2</version>
      </extension>
    </extensions>
    ...
    <plugins>
      ...
      <plugin>
        <groupId>org.xolstice.maven.plugins</groupId>
        <artifactId>protobuf-maven-plugin</artifactId>
        <version>0.6.1</version>
        <configuration>
          <protocArtifact>com.google.protobuf:protoc:3.5.1:exe:${os.detected.classifier}</protocArtifact>
          <pluginId>grpc-java</pluginId>
          <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.28.0:exe:${os.detected.classifier}</pluginArtifact>
          <checkStaleness>true</checkStaleness>
          <protoSourceRoot>${project.build.directory}/proto</protoSourceRoot>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>compile-custom</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      ...
    </plugins>
  </build>
</project>

Add exec-maven-plugin to run the application using Maven.

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>exec-maven-plugin</artifactId>
  <version>1.6.0</version>
  <configuration>
    <mainClass>io.singularitynet.sdk.tutorial.App</mainClass>
  </configuration>
</plugin>

Gradle

Add Maven Central and Jitpack repositories and apply SingularityNET and Protobuf plugins in build.gradle file.

buildscript {
    repositories {
        jcenter()
        maven {
            url 'https://jitpack.io'
        }
    }
    dependencies {
        classpath 'com.github.singnet.snet-sdk-java:snet-sdk-gradle-plugin:0.4.0'
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
    }
}

apply plugin: 'io.singularitynet.sdk'
apply plugin: 'com.google.protobuf'

Add Jitpack repository and add SingularityNET SDK artifact as a project dependency.

repositories {
    ...
    maven {
        url 'https://jitpack.io'
    }
}

dependencies {
    ...
    implementation 'com.github.singnet.snet-sdk-java:snet-sdk-java:0.4.0'
}

Add new task which uses SingularityNET plugin to get the API of the service and unpack it into the proto directory. Add target directory into the Protobuf source set.

task getExampleServiceApi(type: io.singularitynet.sdk.gradle.GetSingularityNetServiceApi) {
    orgId = 'example-org'
    serviceId = 'example-service'
    javaPackage = 'io.singularitynet.service.exampleservice'
    outputDir = file("$buildDir/proto")
    ethereumJsonRpcEndpoint = new URL('http://localhost:8545')
    // for the custom environment only
    ipfsRpcEndpoint = new URL('http://localhost:5002')
    registryAddress = '0x4e74fefa82e83e0964f0d9f53c68e03f7298a8b2'
}

sourceSets {
    main {
        proto {
            srcDir "$buildDir/proto"
        }
    }
}

Configure Protobuf plugin to compile the API of the service. Add dependency on the task which gets the API.

protobuf {
    protoc { artifact = "com.google.protobuf:protoc:3.5.1" }
    plugins {
        java
        grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.28.0" }
    }
    generateProtoTasks {
        all().each { task ->
            task.dependsOn(getExampleServiceApi)
            task.builtins { remove java }
            task.plugins {
                grpc {}
                java {}
            }
        }
    }
}

Setup SDK

SDK configuration contains properties which are required to initialize a SingularityNET platform client. Most of the properties can be left with default values.

Configuration config = Configuration.newBuilder()
    .setEthereumJsonRpcEndpoint("http://localhost:8545")
    .setIdentityType(Configuration.IdentityType.PRIVATE_KEY)
    .setIdentityPrivateKey(Utils.hexToBytes("04899d5fd471ce68f84a5ec64e2e4b6b045d8b850599a57f5b307024be01f262"))
    // for the custom environment only
    .setIpfsEndpoint("http://localhost:5002")
    .setRegistryAddress(new Address("0x4e74fefa82e83e0964f0d9f53c68e03f7298a8b2"))
    .setMultiPartyEscrowAddress(new Address("0x5c7a4290f6f8ff64c69eeffdfafc8644a4ec3a4e"))
    .build();

Ethereum JSON RPC Endpoint is a required property which selects the Ethereum network and JSON RPC endpoint to use. Experienced Ethereum users can use Infura URL with own project id here, see Infura Getting Started. But for the sake of simplicity we use SingularityNET project id Infura URL which is available as a Configuration constant.

Identity type is a required property which selects the type of the Ethereum identity to use. In our example we use a private key identity. To configure it properly we add a private key via “identity private key” property. Utility method Utils.hexToBytes() is used to convert the hex string containing private key to the array of bytes. Here we use the private key of the Ethereum identity which is predefined in the local environment.

Like in the Maven plugin configuration last three parameters are optional, but we should specify them to play with a custom environment.

Configuration is done and we are ready to create an instance of the Sdk class.

Sdk sdk = new Sdk(config);
try {

    // service client code

} finally {
    sdk.close();
}

Sdk class keeps a connection to the Ethereum endpoint and initializes Ethereum smart contracts API. These resources should be released when an Sdk instance is not needed anymore.

Create service client

Before opening connection to the service we need to specify a payment strategy. OnDemandPaymentChannelPaymentStrategy uses MultiPartyEscrow contract to pay for the service calls. It automatically finds an appropriate payment channel or opens the new one. It extends the expiration date and adds the funds if it is required. It has two integer parameters. First parameter specifies the minimal channel lifetime in Ethereum blocks. Second parameter specifies the number of calls to prepay in the channel.

// 40320 is a week in Ethereum blocks assuming single block is mined in 15 seconds
OnDemandPaymentChannelPaymentStrategy paymentStrategy =
    new OnDemandPaymentChannelPaymentStrategy(40320, 100);

sdk.newServiceClient() call opens a gRPC connection to the service client.

ServiceClient serviceClient = sdk.newServiceClient("example-org",
        "example-service", "default_group", paymentStrategy);
try {

    // service client code

} finally {
    serviceClient.close();
}

Service endpoint group id has to be specified in addition to the organization id and service id. It is used to select a service endpoint to connect. To get a list of the service endpoint groups one can use sdk.getMetadataProvider(String orgId, String serviceId).getServiceMetadata() call. See ServiceMetadata documentation for details.

The service client keeps the opened gRPC connection and it should be closed when not needed.

Call service

Last code snippet is pretty close to the gRPC API usage pattern.

CalculatorBlockingStub stub = serviceClient.getGrpcStub(CalculatorGrpc::newBlockingStub);
Numbers numbers = Numbers.newBuilder()
    .setA(7)
    .setB(6)
    .build();
Result result = stub.mul(numbers);
System.out.println("Response received: " + result);

First we create gRPC stub for the gRPC interface we are going to use. Then construct gRPC request, call the service and print the response. Both synchronous and asynchronous gRPC stubs are supported.

Run application

Compile and run the application. If you are using Maven execute:

mvn package exec:java

If it goes well you should see the following response on your console.

Response received: value: 42.0

On the real Ethereum network first call can take much more time than others. The reason is the payment strategy sends an Ethereum transaction in order to prepare the payment channel when appropriate channel is not found. The time to mine the transaction depends on the current gas price and other conditions. On mainnet it may take 1 minute or much more. Consequent calls are much faster because the usage of the payment channel doesn’t require making transactions.

There are two ways of making first call execution time predictable. First option is creating a payment channel in advance. Use Sdk and BlockchainPaymentChannelManager to open a channel from the application. Or use snet-cli tool to open a channel from the command line.

Second option is increasing a gas price by setting new value in configuration, see Configuration.Builder documentation. This way doesn’t guarantee the execution time but can decrease the time to mine the transaction.