Polkadot node provides a convenient RPC API to access most of the functions through HTTP and WebSockets. The API is based on JSON-RPC 2.0 protocol, see https://www.jsonrpc.org/ for the specification.
Polkaj provides several modules to access such APIs, and to encode/decode JSON data to Java classes. Please note, that Polkaj tries, where it’s possible, to map Polkadot specific data types to Java native types.
JSON mapping is based on Jackson JSON library.
dependencies { implementation 'io.emeraldpay.polkaj:polkaj-json-types:0.3.0' }
The library implements serialization and deserialization for the following common types:
-
Hash256
-
Bytes
-
Long as hexadecimal
Plus mapping for standard JSON RPC types:
-
Block
-
Methods List
-
Peer
-
Runtime Version
-
System Health
To configure the JSON mapping you need to register Polkadot Module in Jackson ObjectMapper:
ObjectMapper objectMapper;
objectMapper.registerModule(new PolkadotModule());
If you need to map a custom object, you can define all standard Polkadot as usual:
public class Something {
// When converted to JSON it will be serialized as a hexadecimal string with 0x prefix
// And can be read from the same format automatically
private Hash256 hash;
public Hash256 getHash() {
return hash;
}
public void setHash(Hash256 hash) {
this.hash = hash;
}
}
If you also want to map Java Long
as Polkadot hex numbers, you need to specify a serializer and a deserializer.
So Jackson would encode it as hex, instead of a plain number.
public class Something {
// Configure Jackson to read and write number in hex format with 0x prefix
@JsonDeserialize(using = HexLongDeserializer.class)
@JsonSerialize(using = HexLongSerializer.class)
private Long number;
}
If you encode such an object as JSON:
Something x = new Something();
x.setNumber(123L);
System.out.println(objectMapper.writeValueAsString(x));
You’ll get 0x7b
hex value, instead of plain 123:
{"number":"0x7b"}
The same logic works when you read the JSON into the Something
instance.
Polkaj provides a standardized client to access JSON RPC protocol via HTTP.
It’s based on the standard HttpClient
provided by Java 11.
dependencies { implementation 'io.emeraldpay.polkaj:polkaj-api-http:VERSION' }
To create a new client:
import io.emeraldpay.polkaj.api.PolkadotApi;
import io.emeraldpay.polkaj.apihttp.JavaHttpAdapter;
PolkadotApi api = PolkadotApi.newBuilder()
.rpcCallAdapter(JavaHttpAdapter.newBuilder().build())
.build();
The default configuration is going to connect to http://127.0.0.1:9933
, which is a default configuration of a Polkadot node.
If you need to configure the client, follow the builder methods:
PolkadotApi api = PolkadotApi.newBuilder()
.rpcCallAdapter(JavaHttpAdapter.newBuilder()
.rpcCoder(new RpcCoder(objectMapper)) // (1)
.connectTo("http://10.0.1.20:9333") // (2)
.basicAuth("alice", "secret") // (3)
.build())
.build();
-
Specify an existing ObjectMapper instance to use by the client
-
Connect to a node at
http://10.0.1.20:9333
-
Using Basic Authorization with username
alice
and passwordsecret
The PolkadotApi
has the method execute
that makes an actual call and returns Future
as the result of the call.
interface PolkadotApi {
<T> CompletableFuture<T> execute(RpcCall<T> call);
//...
}
where RpcCall can be created as:
RpcCall.create(
Class<T> clazz, // (1)
String method, // (2)
Object... params // (3)
);
-
expected type of the result
-
method name
-
(optional) parameters
So if you call it as:
Future<String> result = client.execute(RpcCall.create(String.class, "hello_world"))
It should generate JSON RPC call like:
{
"jsonrpc": "2.0",
"id": 0,
"method": "hello_world",
"params": []
}
And if the server respond with:
{
"jsonrpc": "2.0",
"id": 0,
"result": "Hello World!"
}
Then you’ll get that "Hello World!"
as the result of future (result.get()
).
Note
|
A unique numeric id for each request is automatically set by the client, which keeps a sequence of ids and increments it for each request.
|
In case of a JSON response with error field, a RpcException
is thrown during Future .get()
.
The exception contains the code and the message from the original JSON.
Calling most of the Polkadot API methods is straightforward, you just need to specify the right result class.
The result class parameter is used only for convenience and if needed, you can always pass a generic one (e.g. Map.class
) for flexibility.
To get current head of the chain, call chain_getFinalisedHead
which returns Hash256
:
Future<Hash256> hashFuture = client.execute(
// use RpcCall.create to define the request
// the first parameter is Class / JavaType of the expected result
// second is the method name
// and optionally a list of parameters for the call
RpcCall.create(Hash256.class, "chain_getFinalisedHead")
);
Hash256 hash = hashFuture.get();
System.out.println("Current head: " + hash);
And to get a block for a specified hash, call chain_getBlock
with the hash value as the parameter.
It returns BlockResponseJson
class, with block
(use .getBlock()
getter) and justification
fields:
Hash256 hash = ...;
Future<BlockResponseJson> blockFuture = client.execute(
// Another way to prepare a call, instead of manually constructing RpcCall instances
// is to use standard commands provided by PolkadotApi.commands()
// the following line is same as calling it with
// RpcCall.create(BlockResponseJson.class, "chain_getBlock", hash)
PolkadotApi.commands().getBlock(hash)
);
BlockResponseJson block = blockFuture.get();
System.out.println("Block number: " + block.getBlock().getHeader().getNumber());
For the list of all standard commands please see API Commands Reference list
It’s common in Polkadot to return (or accept as parameter) a complex object, usually encoded with Hex and SCALE. Here is an example of how you handle it and decode the data to a Java object.
Future<Metadata> metadataFuture = client.execute(StandardCommands.getInstance().stateMetadata()) // (1) .thenApply(ScaleExtract.fromBytesData(new MetadataReader())); // (2)
-
Requesting Runtime Metadata which describes all configured modules and storages for the blockchain
-
And then apply ScaleReader implemented with class
MetadataReader
The Metadata
class, as well as MetadataReader
are provided by the module io.emeraldpay.polkaj:polkaj-scale-types:0.3.0
In addition to HTTP based JSON RPC protocol, Polkadot nodes provide WebSocket based API. It allows subscribing to the events happening on the blockchain, such as changing of the Head block.
JavaHttpSubscriptionAdapter wsAdapter = JavaHttpSubscriptionAdapter.newBuilder().build();
PolkadotApi api = PolkadotApi.newBuilder()
.subscriptionAdapter(wsAdapter)
.build();
// IMPORTANT! connect to the node as the first step before making calls or subscriptions.
wsAdapter.connect().get(5, TimeUnit.SECONDS);
Class JavaHttpSubscriptionAdapter
implements SubscriptionAdapter
When setting a SubscriptionAdapter
via the Builder, it will also be used as the RpcAdapter
interface PolkadotApi {
//...
<T> CompletableFuture<Subscription<T>> subscribe(SubscribeCall<T> call);
}
The Subscription<T>
instance provides method handler()
to handle events.
Note that if handler is added twice, a new handler replaces the previous one.
Providing a null handler removes the handler.
interface Subscription<T> extends AutoCloseable {
void handler(Consumer<? extends Subscription.Event<? extends T>> handler);
}
And the Event itself is a simple wrapper around the JSON-based result, with a method name provided by the subscription response. For the most of the cases you can ignore latter (method name) and use the result only.
class Event<T> {
public T getResult();
public String getMethod();
}
The first thing you have to do (after .connect
) is to start the subscription by sending a command.
You can construct SubscribeCall<T>
manually, or use a command from the predefines set of standard subscriptions provided by PolkadotSubscriptionApi.subscriptions()
For the complete list of all standard subscriptions please see API Commands Reference list
In the example below we subscribe to new heads, i.e. to the headers of the new blocks on the top of the blockchain. We then wait for subscription to be confirmed for 5 seconds. Note, it’s the time we wait for the response from the server that provide use with subscription, not for the events itself.
Future<Subscription<BlockJson.Header>> hashFuture = api.subscribe(
PolkadotSubscriptionApi.subscriptions().newHeads()
);
Subscription<BlockJson.Header> subscription = hashFuture.get(5, TimeUnit.SECONDS);
Once we got the Subscription we can add a handler to it, which in the example below just prints block header info to the console.
subscription.handler((Subscription.Event<BlockJson.Header> event) -> {
BlockJson.Header header = event.getResult();
List<String> line = List.of(
Instant.now().truncatedTo(ChronoUnit.SECONDS).toString(),
header.getNumber().toString(),
header.getStateRoot().toString()
);
System.out.println(String.join("\t", line));
});
Since the SubscriptionAdapter
extends standard RpcAdapter
you can make all other calls through the same WebSocket connection:
Future<BlockResponseJson> previousBlock = api.execute(
PolkadotApi.commands().getBlock(header.getParentHash())
);