Structuring Blockchain APIs with Protocol Buffers
Too many coins, tokens and Blockchains..
First, we need protobuf compiler tools. on a mac, Protobuf tools can be installed from here and here.
I'm gonna write a coin manager gRPC service that will fetch balance from various APIs.
let's start with .proto
syntax = "proto3";
message BalanceRequest {
string coin = 1;
string address = 2;
}
message BalanceResponse {
string coin = 1;
string address = 2;
string balance = 3;
string error = 4;
}
service CoinService {
rpc Balance (BalanceRequest) returns (BalanceResponse);
}
.proto file defines the RPC method 'Balance' and it's input / output params structure.
the numbers denote the field number which has significance in decoding / encoding rpc raw data and the same can be used for preserving backward compatibility as well.
this CoinService defines single RPC method 'Balance'.
corresponding language specific libraries for bootstrapping RPC server can be generated using grpc tools. Following is a way to generate Ruby classes with above mentioned grpc service and methods.
grpc_tools_ruby_protoc -I src --ruby_out=build/gen/lib --grpc_out=build/gen/lib src/coin_service.proto
One can find the generated libs as follows which contains RPC methods and params structures.
types file
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: coin_service.proto
require 'google/protobuf'
Google::Protobuf::DescriptorPool.generated_pool.build do
add_message "BalanceRequest" do
optional :coin, :string, 1
optional :address, :string, 2
end
add_message "BalanceResponse" do
optional :coin, :string, 1
optional :address, :string, 2
optional :balance, :string, 3
optional :error, :string, 4
end
end
BalanceRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("BalanceRequest").msgclass
BalanceResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("BalanceResponse").msgclass
service file
# Generated by the protocol buffer compiler. DO NOT EDIT!
# Source: coin_service.proto for package ''
require 'grpc'
require 'coin_service_pb'
module CoinService
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
self.service_name = 'CoinService'
rpc :Balance, BalanceRequest, BalanceResponse
end
Stub = Service.rpc_stub_class
end
a sinatra client that exposes this RPC API can be written as follows
# coin_client.rb
this_dir = File.expand_path(File.dirname(__FILE__))
lib_dir = File.join(this_dir, 'lib')
$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
require 'coin_service_services_pb'
require 'coin_service_pb'
require 'sinatra'
require "sinatra/json"
def get_balance(params)
begin
stub = CoinService::Stub.new('localhost:50051', :this_channel_is_insecure)
resp = stub.balance(BalanceRequest.new(address: params[:address], coin: params[:coin]))
{balance: resp.balance, coin: resp.coin, address: resp.address}
rescue => e
{error: e.message}
end
end
get '/' do
json (get_balance params)
end
'BalanceRequest' will through exceptions if any additional params is passed or if any required params were missing. This ensures nothing more - nothing less is passed to server.
stub.balance makes a RPC call to endpoint localhost:50051 where the gRPC server should listen for RPC calls. The response type of RPC call is preserved as declared 'BalanceResponse'.
Server that handles gRPC requests is written as follows
this_dir = File.expand_path(File.dirname(__FILE__))
lib_dir = File.join(this_dir, 'lib')
$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
require 'coin_service_services_pb'
require 'coin_service_pb'
require 'rest-client'
require 'json'
require 'coin_config'
class CoinServer < CoinService::Service
def balance(balance_req, _unused_call)
api = CoinCoinfig.balance_api(balance_req.address, balance_req.coin)
resp = RestClient.get(api[:url].())
data = api[:parse].(resp.body)
BalanceResponse.new({coin: balance_req.coin, address: balance_req.address}.merge(data))
end
end
def init_server
s = GRPC::RpcServer.new
s.add_http2_port('0.0.0.0:50051', :this_port_is_insecure)
s.handle(CoinServer)
s.run_till_terminated
end
init_server
coin config file
# lib/coin_config.rb
class CoinCoinfig
def self.contract_address(coin)
{
'zrx' => '0xe41d2489571d322189246dafa5ebde1f4699f498',
'omg' => '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
'dnt' => '0x0abdace70d3790235af448c88547603b945604ea'
}[coin] || (raise "Invalid Coin")
end
def self.token?(coin)
['zrx', 'omg', 'dnt'].include?(coin)
end
def self.balance_api(address, coin)
{
'eth' => { url: -> { "https://api.etherscan.io/api?module=account&action=balance&address=#{address}&tag=latest&apikey=YourApiKeyToken" },
parse: -> response {
{balance: JSON.parse(response)["result"]}
}
},
'token' => { url: -> { "https://api.etherscan.io/api?module=account&action=tokenbalance&contractaddress=#{CoinCoinfig.contract_address(coin)}&address=#{address}&tag=latest&apikey=YourApiKeyToken" },
parse: -> response {
{balance: JSON.parse(response)["result"]}
}
},
'btc' => { url: -> { "https://blockchain.info/rawaddr/#{address}" },
parse: -> response {
{balance: (JSON.parse(response)["final_balance"] / 1e8).to_s}
}
}
}[token?(coin) ? 'token' : coin]
end
end
Use of Procs in url is to make the string interpolation lazy and not to realize params until correct coin is determined.
one can find balance of BTC, ETH or any Token configured in coin_config.rb by running the gRPC server and client.
# TTY1
ruby build/gen/coin_server.rb
# TTY2
ruby build/gen/coin_client.rb
Puma starting in single mode...
* Version 3.12.0 (ruby 2.4.1-p111), codename: Llamas in Pajamas
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://localhost:4567
Use Ctrl-C to stop
BTC balance
Token Balance
ETH Balance
Use of Rest APIs to interact with various blockchains may not sound much significant to use with Protobuf. Since almost all blockchain provides RPC API, methods like generateAddress, getBalance, newAccount are common functions for all blockchains which can be structured with Protobuf and corresponding client code can be generated for various languages from JS to ruby to golang. and with gRPC, all the pros of HTTP 2.0 comes along with it to your microservices.