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.
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
.
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:
orgId
and serviceId
parameters specify the service we are going to use;outputDir
points to the location which is used to download the Protobuf API
of the service;javaPackage
sets the convenient package name to place the compiled API
classes;ethereumJsonRpcEndpoint
specifies the Ethereum network and RPC endpoint to
use.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.
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>
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 {}
}
}
}
}
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.
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.
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.
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.