class Bluzelle::Client

Public Class Methods

new(options) click to toggle source
# File lib/bluzelle.rb, line 64
def initialize(options)
  @options = options
end

Public Instance Methods

api_mutate(method, endpoint, payload) click to toggle source
# File lib/bluzelle.rb, line 284
def api_mutate(method, endpoint, payload)
  url = @options['endpoint'] + endpoint
  @logger.debug("mutating url(#{url}), method(#{method})")
  uri = URI(url)
  http = Net::HTTP.new(uri.host, uri.port)
  if method == "delete"
    request = Net::HTTP::Delete.new(uri.request_uri)
  else
    request = Net::HTTP::Post.new(uri.request_uri)
  end
  request['Accept'] = 'application/json'
  request.content_type = 'application/json'
  data = Bluzelle::json_dumps payload
  @logger.debug("data(#{data})")
  request.body = data
  response = http.request(request)
  @logger.debug("response (#{response.body})...")
  data = JSON.parse(response.body)
  error = get_response_error(data)
  raise error if error
  data
end
api_query(endpoint) click to toggle source
# File lib/bluzelle.rb, line 273
def api_query(endpoint)
  url = @options['endpoint'] + endpoint
  @logger.debug("querying url(#{url})...")
  response = Net::HTTP.get_response URI(url)
  data = JSON.parse(response.body)
  error = get_response_error(data)
  raise error if error
  @logger.debug("response (#{data})...")
  data
end
broadcast_transaction(data, gas_info: nil) click to toggle source
# File lib/bluzelle.rb, line 326
def broadcast_transaction(data, gas_info: nil)
  # fee
  fee = data['fee']
  fee_gas = fee['gas'].to_i
  if gas_info == nil
    gas_info = @options['gas_info']
  end
  if gas_info == nil
    raise OptionsError, 'please provide gas_info when initializing the client or in the transaction'
  end
  Bluzelle::validate_gas_info gas_info
  if gas_info['max_gas'] != 0 && fee_gas > gas_info['max_gas']
    fee['gas'] = gas_info['max_gas'].to_s
  end
  if gas_info['max_fee'] != 0
    fee['amount'] = [{ 'denom' => TOKEN_NAME, 'amount' => gas_info['max_fee'].to_s }]
  elsif gasInfo['gas_price'] != 0
    fee['amount'] = [{ 'denom' => TOKEN_NAME, 'amount' => (fee_gas * gas_info['gas_price']).to_s }]
  end

  # sort
  txn = build_txn(
    fee: fee,
    msg: data['msg'][0]
  )

  # signatures
  txn['signatures'] = [{
    'account_number' => @account['account_number'].to_s,
    'pub_key' => {
      'type' => PUB_KEY_TYPE,
      'value' => Bluzelle::base64_encode(@wallet.public_key.compressed.to_bytes)
    },
    'sequence' => @account['sequence'].to_s,
    'signature' => sign_transaction(txn)
  }]

  payload = { 'mode' => 'block', 'tx' => txn }
  response = api_mutate('post', TX_COMMAND, payload)

  # https://github.com/bluzelle/blzjs/blob/45fe51f6364439fa88421987b833102cc9bcd7c0/src/swarmClient/cosmos.js#L240-L246
  # note - as of right now (3/6/20) the responses returned by the Cosmos REST interface now look like this:
  # success case: {"height":"0","txhash":"3F596D7E83D514A103792C930D9B4ED8DCF03B4C8FD93873AB22F0A707D88A9F","raw_log":"[]"}
  # failure case: {"height":"0","txhash":"DEE236DEF1F3D0A92CB7EE8E442D1CE457EE8DB8E665BAC1358E6E107D5316AA","code":4,
  #  "raw_log":"unauthorized: signature verification failed; verify correct account sequence and chain-id"}
  #
  # this is far from ideal, doesn't match their docs, and is probably going to change (again) in the future.
  unless response.fetch('code', nil)
    @account['sequence'] += 1
    if response.fetch('data', nil)
      return JSON.parse Bluzelle::hex_to_ascii response['data']
    end
    return
  end

  raw_log = response["raw_log"]
  if raw_log.include?("signature verification failed")
    @broadcast_retries += 1
    @logger.warn("transaction failed ... retrying(#{@broadcast_retries}) ...")
    if @broadcast_retries >= BROADCAST_MAX_RETRIES
      raise APIError, "transaction failed after max retry attempts"
    end

    sleep BROADCAST_RETRY_INTERVAL_SECONDS
    set_account()
    broadcast_transaction(txn, gas_info: gas_info)
    return
  end

  raise APIError, raw_log
end
build_txn(fee:, msg:) click to toggle source
# File lib/bluzelle.rb, line 417
def build_txn(fee:, msg:)
  # TODO: find a better way to sort
  fee_amount = fee['amount'][0]
  txn = {
    'fee' => {
      "amount" => [
        {
          "amount" => fee_amount['amount'],
          "denom" => fee_amount['denom']
        }
      ],
      "gas" => fee['gas']
    },
    'memo' => Bluzelle::make_random_string(32)
  }
  msg_value = msg['value']
  sorted_msg_value = {}
  MSG_KEYS_ORDER.each do |key|
    val = msg_value.fetch(key, nil)
    sorted_msg_value[key] = val if val
  end
  txn['msg'] = [
    {
      "type" => msg['type'],
      "value" => sorted_msg_value
    }
  ]
  txn
end
count() click to toggle source
# File lib/bluzelle.rb, line 201
def count()
  url = "/crud/count/#{@options["uuid"]}"
  api_query(url)["result"]["count"].to_i
end
create(key, value, lease_info: nil, gas_info: nil) click to toggle source

mutate

# File lib/bluzelle.rb, line 105
def create(key, value, lease_info: nil, gas_info: nil)
  payload = {"Key" => key}
  payload["Lease"] = Bluzelle::lease_info_to_blocks(lease_info).to_s if lease_info
  payload["Value"] = value
  send_transaction(
    'post',
    '/crud/create',
    payload,
    gas_info: gas_info
  )
end
delete(key, gas_info: nil) click to toggle source
# File lib/bluzelle.rb, line 129
def delete(key, gas_info: nil)
  send_transaction(
    "delete",
    "/crud/delete",
    {"Key" => key},
    gas_info: gas_info
  )
end
delete_all(gas_info: nil) click to toggle source
# File lib/bluzelle.rb, line 147
def delete_all(gas_info: nil)
  send_transaction(
    "post",
    "/crud/deleteall",
    {},
    gas_info: gas_info
  )
end
get_lease(key) click to toggle source
# File lib/bluzelle.rb, line 216
def get_lease(key)
  url = "/crud/getlease/#{@options["uuid"]}/#{key}"
  Bluzelle::lease_blocks_to_seconds api_query(url)["result"]["lease"].to_i
end
get_n_shortest_leases(n) click to toggle source
# File lib/bluzelle.rb, line 221
def get_n_shortest_leases(n)
  url = "/crud/getnshortestleases/#{@options["uuid"]}/#{n}"
  kls = api_query(url)["result"]["keyleases"]
  kls.each do |kl|
    kl["lease"] = Bluzelle::lease_blocks_to_seconds kl["lease"]
  end
  kls
end
get_response_error(response) click to toggle source
# File lib/bluzelle.rb, line 447
def get_response_error(response)
  error = response['error']
  return APIError.new(error) if error
end
has(key) click to toggle source
# File lib/bluzelle.rb, line 196
def has(key)
  url = "/crud/has/#{@options["uuid"]}/#{key}"
  api_query(url)["result"]["has"]
end
key_values() click to toggle source
# File lib/bluzelle.rb, line 211
def key_values()
  url = "/crud/keyvalues/#{@options["uuid"]}"
  api_query(url)["result"]["keyvalues"]
end
keys() click to toggle source
# File lib/bluzelle.rb, line 206
def keys()
  url = "/crud/keys/#{@options["uuid"]}"
  api_query(url)["result"]["keys"]
end
multi_update(payload, gas_info: nil) click to toggle source
# File lib/bluzelle.rb, line 156
def multi_update(payload, gas_info: nil)
  list = []
  payload.each do |key, value|
    list.append({"key" => key, "value" => value})
  end
  send_transaction(
    "post",
    "/crud/multiupdate", {"KeyValues" => list},
    gas_info: gas_info
  )
end
proven_read(key) click to toggle source
# File lib/bluzelle.rb, line 191
def proven_read(key)
  url = "/crud/pread/#{@options["uuid"]}/#{key}"
  api_query(url)["result"]["value"]
end
read(key) click to toggle source

query

# File lib/bluzelle.rb, line 186
def read(key)
  url = "/crud/read/#{@options["uuid"]}/#{key}"
  api_query(url)["result"]["value"]
end
read_account() click to toggle source
# File lib/bluzelle.rb, line 93
def read_account
  url = "/auth/accounts/#{@options['address']}"
  api_query(url)['result']['value']
end
rename(key, new_key, gas_info: nil) click to toggle source
# File lib/bluzelle.rb, line 138
def rename(key, new_key, gas_info: nil)
  send_transaction(
    "post",
    "/crud/rename",
    {"Key" => key, "NewKey" => new_key},
    gas_info: gas_info
  )
end
renew_all_leases(lease_info, gas_info: nil) click to toggle source
# File lib/bluzelle.rb, line 176
def renew_all_leases(lease_info, gas_info: nil)
  send_transaction(
    "post", "/crud/renewleaseall",
    {"Lease" => Bluzelle::lease_info_to_blocks(lease_info).to_s},
    gas_info: gas_info
  )
end
renew_lease(key, lease_info, gas_info: nil) click to toggle source
# File lib/bluzelle.rb, line 168
def renew_lease(key, lease_info, gas_info: nil)
  send_transaction(
    "post", "/crud/renewlease",
    {"Key" => key, "Lease" => Bluzelle::lease_info_to_blocks(lease_info).to_s},
    gas_info: gas_info
  )
end
send_transaction(method, endpoint, payload, gas_info: nil) click to toggle source
# File lib/bluzelle.rb, line 307
def send_transaction(method, endpoint, payload, gas_info: nil)
  @broadcast_retries = 0
  txn = validate_transaction(method, endpoint, payload)
  broadcast_transaction(txn, gas_info: gas_info)
end
set_account() click to toggle source
# File lib/bluzelle.rb, line 87
def set_account
  @account = read_account
end
set_private_key() click to toggle source
# File lib/bluzelle.rb, line 73
def set_private_key
  seed = BipMnemonic.to_seed(mnemonic: @options['mnemonic'])
  master = MoneyTree::Master.new(seed_hex: seed)
  @wallet = master.node_for_path(HD_PATH)
end
setup_logging() click to toggle source
# File lib/bluzelle.rb, line 68
def setup_logging
  @logger = Logger.new(STDOUT)
  @logger.level = if @options['debug'] then Logger::DEBUG else Logger::FATAL end
end
sign_transaction(txn) click to toggle source
# File lib/bluzelle.rb, line 398
def sign_transaction(txn)
  payload = {
    'account_number' => @account['account_number'].to_s,
    'chain_id' => @options['chain_id'],
    'fee' => txn['fee'],
    'memo' => txn['memo'],
    'msgs' => txn['msg'],
    'sequence' => @account['sequence'].to_s
  }
  payload = Bluzelle::json_dumps(payload)

  pk = Secp256k1::PrivateKey.new(privkey: @wallet.private_key.to_bytes, raw: true)
  rs = pk.ecdsa_sign payload
  r = rs.slice(0, 32).read_string.reverse
  s = rs.slice(32, 32).read_string.reverse
  sig = "#{r}#{s}"
  Bluzelle::base64_encode sig
end
tx_count(gas_info: nil) click to toggle source
# File lib/bluzelle.rb, line 242
def tx_count(gas_info: nil)
  res = send_transaction("post", "/crud/count", {}, gas_info: gas_info)
  res["count"].to_i
end
tx_get_lease(key, gas_info: nil) click to toggle source
# File lib/bluzelle.rb, line 257
def tx_get_lease(key, gas_info: nil)
  res = send_transaction("post", "/crud/getlease", {"Key" => key}, gas_info: gas_info)
  Bluzelle::lease_blocks_to_seconds res["lease"].to_i
end
tx_get_n_shortest_leases(n, gas_info: nil) click to toggle source
# File lib/bluzelle.rb, line 262
def tx_get_n_shortest_leases(n, gas_info: nil)
  res = send_transaction("post", "/crud/getnshortestleases", {"N" => n.to_s}, gas_info: gas_info)
  kls = res["keyleases"]
  kls.each do |kl|
    kl["lease"] = Bluzelle::lease_blocks_to_seconds kl["lease"]
  end
  kls
end
tx_has(key, gas_info: nil) click to toggle source
# File lib/bluzelle.rb, line 237
def tx_has(key, gas_info: nil)
  res = send_transaction("post", "/crud/has", {"Key" => key}, gas_info: gas_info)
  res["has"]
end
tx_key_values(gas_info: nil) click to toggle source
# File lib/bluzelle.rb, line 252
def tx_key_values(gas_info: nil)
  res = send_transaction("post", "/crud/keyvalues", {}, gas_info: gas_info)
  res["keyvalues"]
end
tx_keys(gas_info: nil) click to toggle source
# File lib/bluzelle.rb, line 247
def tx_keys(gas_info: nil)
  res = send_transaction("post", "/crud/keys", {}, gas_info: gas_info)
  res["keys"]
end
tx_read(key, gas_info: nil) click to toggle source
# File lib/bluzelle.rb, line 232
def tx_read(key, gas_info: nil)
  res = send_transaction("post", "/crud/read", {"Key" => key}, gas_info: gas_info)
  res["value"]
end
update(key, value, lease_info: nil, gas_info: nil) click to toggle source
# File lib/bluzelle.rb, line 117
def update(key, value, lease_info: nil, gas_info: nil)
  payload = {"Key" => key}
  payload["Lease"] = Bluzelle::lease_info_to_blocks(lease_info).to_s if lease_info
  payload["Value"] = value
  send_transaction(
    "post",
    "/crud/update",
    payload,
    gas_info: gas_info
  )
end
validate_transaction(method, endpoint, payload) click to toggle source
# File lib/bluzelle.rb, line 313
def validate_transaction(method, endpoint, payload)
  address = @options['address']
  payload = payload.merge({
    'BaseReq' => {
      'chain_id' => @options['chain_id'],
      'from' => address
    },
    'Owner' => address,
    'UUID' => @options['uuid']
  })
  api_mutate(method, endpoint, payload)['value']
end
verify_address() click to toggle source
# File lib/bluzelle.rb, line 79
def verify_address
  b = Digest::RMD160.digest(Digest::SHA256.digest(@wallet.public_key.compressed.to_bytes))
  address = Bech32.encode(ADDRESS_PREFIX, Bech32.convert_bits(b, from_bits: 8, to_bits: 5, pad: true))
  if address != @options['address']
    raise OptionsError, 'bad credentials(verify your address and mnemonic)'
  end
end
version() click to toggle source
# File lib/bluzelle.rb, line 98
def version()
  url = "/node_info"
  api_query(url)["application_version"]["version"]
end