Structuring Blockchain APIs with Protocol Buffers

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.

要查看或添加评论,请登录

Shanthakumar `的更多文章

  • When Voice Met Code: A Developer’s Quest to Teach AI Context

    When Voice Met Code: A Developer’s Quest to Teach AI Context

    The Promise of Voice-Driven Programming We’re living in a sci-fi-esque era where AI tools let us speak code into…

  • XPATH - Swiss Army Knife of Web Scrapping

    XPATH - Swiss Army Knife of Web Scrapping

    sometimes you can't thank enough to invention of Spreadsheets. XPATH adds another level of power to your complex needs.

  • Let's talk about IPFS now.

    Let's talk about IPFS now.

    Instantiate IPFS node const node = new Ipfs({ repo: 'ipfs-pluto', pass: 'cutedoll', ? ? ? EXPERIMENTAL: {…

    1 条评论
  • ES6 Proxy Objects and Magic Methods

    ES6 Proxy Objects and Magic Methods

    Recently I've used Proxy for a piece of code to maintain application state and manipulate it according to async API…

社区洞察

其他会员也浏览了