lib/tezos_client.rb
# frozen_string_literal: true
require "pp"
require "active_support/core_ext/hash/indifferent_access"
require "active_support/core_ext/string/inflections"
require "active_support/core_ext/module/delegation"
require "active_support/core_ext/time"
require "active_support/core_ext/array"
require "timeout"
require "benchmark"
require "open3"
require "active_interaction"
require "time"
require "tezos_client/version"
require "tezos_client/string_utils"
require "tezos_client/currency_utils"
require "tezos_client/crypto"
require "tezos_client/commands"
require "tezos_client/logger"
require "tezos_client/exceptions"
require "tezos_client/encode_utils"
require "tezos_client/operation_mgr"
require "tezos_client/operations/operation"
require "tezos_client/operations/origination_operation"
require "tezos_client/operations/transaction_operation"
require "tezos_client/operations/transactions_operation"
require "tezos_client/operations/activate_account_operation"
require "tezos_client/operations/reveal_operation"
require "tezos_client/operations/raw_operation_array"
require "tezos_client/operations/operation_array"
require "tezos_client/tools/system_call"
require "tezos_client/tools/temporary_file"
require "tezos_client/rpc_interface"
require "tezos_client/smartpy_interface"
require "tezos_client/tools/convert_to_hash"
require "tezos_client/tools/find_big_maps_in_storage"
require "tezos_client/tools/hash_to_micheline"
require "tezos_client/tools/annots_to_type"
class TezosClient
using CurrencyUtils
using StringUtils
include Logger
include Commands
include Crypto
attr_accessor :rpc_interface
attr_accessor :smartpy_interface
RANDOM_SIGNATURE = "edsigu165B7VFf3Dpw2QABVzEtCxJY2gsNBNcE3Ti7rRxtDUjqTFRpg67EdAQmY6YWPE5tKJDMnSTJDFu65gic8uLjbW2YwGvAZ"
def initialize(rpc_node_address: "127.0.0.1", rpc_node_port: 8732, liquidity_options: {})
@rpc_node_address = rpc_node_address
@rpc_node_port = rpc_node_port
@client_config_file = ENV["TEZOS_CLIENT_CONFIG_FILE"]
@rpc_interface = RpcInterface.new(
host: @rpc_node_address,
port: @rpc_node_port
)
@smartpy_interface = SmartpyInterface.new
end
# Originates a contract on the tezos blockchain
#
# @param from [String] Address originating the contract
# @param amount [Numeric] amount to send to the contract
# @param secret_key [String] Secret key of the origination address
# @param args [Hash] keyword options for the origination
# @option args [String] :script path of the liquidity script
# @option args [Array, String] :init_params params to pass to the storage initialization process
# @option args [Boolean] :spendable decide wether the contract is spendable or not
# @option args [Boolean] :delegatable decide wether the contract is delegatable or not
#
# @return [Hash] result of the origination containing :operation_id, :operation_result and :originated_contract
#
def originate_contract(from:, amount:, secret_key: nil, script: nil, init_params: [], dry_run: false, **args)
origination_args = {
rpc_interface: rpc_interface,
from: from,
secret_key: secret_key,
amount: amount,
**args
}
origination_args[:script] = contract_interface(script).origination_script(
script: script,
init_params: init_params,
**args
)
operation = OriginationOperation.new(**origination_args)
res = broadcast_operation(operation: operation, dry_run: dry_run)
res.merge(
originated_contract: res[:operation_results][0][:originated_contracts][0]
)
end
# Transfer funds to an account
#
# @param from [String] Address originating the transfer
# @param to [String] Address receiving the transfer
# @param amount [Numeric] amount to send to the account
# @param secret_key [String] Secret key of the origination address
# @param args [Hash] keyword options for the transfer
#
# @return [Hash] result of the transfer containing :operation_id and :operation_result
#
def transfer(from:, amount:, to:, secret_key:, dry_run: false, **args)
operation = TransactionOperation.new(
rpc_interface: rpc_interface,
from: from,
to: to,
secret_key: secret_key,
amount: amount,
**args
)
broadcast_operation(operation: operation, dry_run: dry_run)
end
def activate_account(pkh:, secret:, dry_run: false, **args)
operation = ActivateAccountOperation.new(
rpc_interface: rpc_interface,
pkh: pkh,
secret: secret,
**args
)
broadcast_operation(operation: operation, dry_run: dry_run)
end
def transfer_to_many(from:, amounts:, secret_key:, dry_run: false, **args)
operation = TransactionsOperation.new(
rpc_interface: rpc_interface,
from: from,
amounts: amounts,
secret_key: secret_key,
**args
)
broadcast_operation(operation: operation, dry_run: dry_run)
end
def reveal_pubkey(secret_key:, dry_run: false, **args)
public_key = secret_key_to_public_key(secret_key)
from = public_key_to_address(public_key)
operation = RevealOperation.new(
rpc_interface: rpc_interface,
public_key: public_key,
from: from,
secret_key: secret_key,
**args
)
broadcast_operation(operation: operation, dry_run: dry_run)
end
def build_transaction(dry_run: false, entrypoint:, params:, params_type:, **args)
transfer_args = transfer_args(dry_run: dry_run, entrypoint: entrypoint, params: params, params_type: params_type, **args)
transaction = TransactionOperation.new(
rpc_interface: rpc_interface,
**transfer_args
)
transaction.rpc_operation_args
end
def call_contract(dry_run: false, entrypoint:, params:, params_type:, **args)
transfer_args = transfer_args(dry_run: dry_run,
entrypoint: entrypoint,
params: params,
params_type: params_type,
**args)
transfer(**transfer_args)
end
def select_entrypoint(contract_address:, entrypoint:)
entrypoints = entrypoints(contract_address)["entrypoints"].keys
if entrypoints.count == 0
"default"
elsif entrypoints.include?(entrypoint)
entrypoint
else
raise ::ArgumentError, "entrypoint #{entrypoint} not found in #{entrypoints}"
end
end
def inject_raw_operations(secret_key:, raw_operations:, dry_run: false, **args)
public_key = secret_key_to_public_key(secret_key)
from = public_key_to_address(public_key)
operation = RawOperationArray.new(
rpc_interface: rpc_interface,
public_key: public_key,
from: from,
secret_key: secret_key,
raw_operations: raw_operations,
**args
)
broadcast_operation(operation: operation, dry_run: dry_run)
end
def monitor_operation(operation_id, timeout: 120)
including_block = nil
monitoring_thread = rpc_interface.monitor_block do |block_header|
log "recently received block: #{block_header.pretty_inspect}"
hash = block_header["hash"]
if block_include_operation?(operation_id, hash)
log "operations #{operation_id} found in block #{hash}"
including_block = hash
end
end
Timeout.timeout(timeout) do
loop do
sleep(0.1)
break unless monitoring_thread.alive?
break unless including_block.nil?
end
end
if monitoring_thread.status.nil?
# when thread raise an Exception, reraise it
log "monitoring thread raised an exception"
monitoring_thread.value
else
monitoring_thread.terminate
end
including_block
end
def block_include_operation?(operation_id, block_id)
retries ||= 0
operations = rpc_interface.get("chains/main/blocks/#{block_id}/operation_hashes")
operations.flatten.include? operation_id
rescue TezosClient::RpcRequestFailure
if (retries += 1) < 3
sleep(2)
retry
else
raise
end
end
def self.root_path
File.expand_path(File.dirname(__FILE__))
end
private
def transfer_args(dry_run: false, entrypoint:, params:, params_type:, **args)
_entrypoint = select_entrypoint(
contract_address: args[:to],
entrypoint: entrypoint
)
json_params = micheline_params(
params: params,
entrypoint: _entrypoint,
params_type: params_type
)
args.merge(
entrypoint: _entrypoint,
parameters: json_params,
dry_run: dry_run
)
end
def broadcast_operation(operation:, dry_run:)
res = if dry_run
operation.simulate
else
operation.test_and_broadcast
end
res.merge(
rpc_operation_args: operation.rpc_operation_args
)
end
def micheline_params(params:, entrypoint:, params_type:)
{
entrypoint: entrypoint,
value: convert_params(
params: params,
params_type: params_type
)
}
end
def convert_params(params:, params_type:)
case params_type.to_sym
when :micheline
params
else
raise ::ArgumentError, "params type must be equal to [ :micheline ]"
end
end
def contract_interface(script)
case script.to_s
when /[A-Za-z_\/-]*.py/
smartpy_interface
when nil
raise "script var unset"
else
raise "unknown contract type"
end
end
end