Class: RailsCryptoPayment::EvmRpc

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_crypto_payment/evm_rpc.rb

Defined Under Namespace

Classes: RpcError

Constant Summary collapse

TRANSFER =
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"

Instance Method Summary collapse

Constructor Details

#initialize(url:, username: nil, password: nil) ⇒ EvmRpc

Returns a new instance of EvmRpc.

Raises:

  • (ArgumentError)


20
21
22
23
24
25
26
27
28
# File 'lib/rails_crypto_payment/evm_rpc.rb', line 20

def initialize(url:, username: nil, password: nil)
  @uri = URI(url.to_s)
  raise ArgumentError, "RPC URL must use HTTP or HTTPS" unless %w[http https].include?(@uri.scheme)
  raise ArgumentError, "RPC username and password must be provided together" if username.to_s.empty? != password.to_s.empty?
  raise ArgumentError, "Credentialed RPC URL must use HTTPS" if @uri.scheme != "https" && !username.to_s.empty?

  @username = username
  @password = password
end

Instance Method Details

#block_time(log) ⇒ Object



79
80
81
82
# File 'lib/rails_crypto_payment/evm_rpc.rb', line 79

def block_time(log)
  block = call("eth_getBlockByNumber", [log.fetch("blockNumber"), false])
  Time.at(Integer(block.fetch("timestamp"), 16)).utc
end

#call(method, params) ⇒ Object



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/rails_crypto_payment/evm_rpc.rb', line 30

def call(method, params)
  request = Net::HTTP::Post.new(@uri, "Content-Type" => "application/json")
  request.basic_auth(@username, @password) if @username && @password
  request.body = { jsonrpc: "2.0", id: 1, method: method, params: params }.to_json
  response = Net::HTTP.start(@uri.host, @uri.port, use_ssl: @uri.scheme == "https", open_timeout: 5, read_timeout: 15) { |http| http.request(request) }
  raise Error, "RPC HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)

  body = JSON.parse(response.body)
  raise RpcError.new(body["error"]["code"], body["error"]["message"]) if body["error"]

  body.fetch("result")
rescue Error
  raise
rescue StandardError => e
  raise Error, "RPC request failed: #{e.message}"
end

#transfer_amount(log, decimals:) ⇒ Object

Raises:

  • (ArgumentError)


72
73
74
75
76
77
# File 'lib/rails_crypto_payment/evm_rpc.rb', line 72

def transfer_amount(log, decimals:)
  scale = Integer(decimals)
  raise ArgumentError, "decimals must be nonnegative" if scale.negative?

  BigDecimal(Integer(log.fetch("data"), 16).to_s) / 10**scale
end

#transfer_logs(from_block:, to_block:, token:, wallets:) ⇒ Object



47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/rails_crypto_payment/evm_rpc.rb', line 47

def transfer_logs(from_block:, to_block:, token:, wallets:)
  raise ArgumentError, "from_block must not exceed to_block" if from_block > to_block

  topics = wallets.map { |wallet| "0x#{wallet.delete_prefix('0x').downcase.rjust(64, '0')}" }
  call("eth_getLogs", [{ fromBlock: "0x#{from_block.to_s(16)}", toBlock: "0x#{to_block.to_s(16)}",
    address: token, topics: [TRANSFER, nil, topics] }])
rescue RpcError => e
  raise unless e.code == -32_005 && from_block < to_block

  middle = (from_block + to_block) / 2
  transfer_logs(from_block: from_block, to_block: middle, token: token, wallets: wallets) +
    transfer_logs(from_block: middle + 1, to_block: to_block, token: token, wallets: wallets)
end

#valid_transfer?(log:, token:) ⇒ Boolean

Returns:

  • (Boolean)


61
62
63
64
65
66
67
68
69
70
# File 'lib/rails_crypto_payment/evm_rpc.rb', line 61

def valid_transfer?(log:, token:)
  return false if log["removed"] || !log.fetch("address").casecmp?(token) ||
    !log.fetch("topics").first.casecmp?(TRANSFER)

  receipt = call("eth_getTransactionReceipt", [log.fetch("transactionHash")])
  receipt && receipt["status"] == "0x1" && receipt.fetch("logs").any? do |entry|
    entry["logIndex"] == log["logIndex"] && entry.fetch("address").casecmp?(token) &&
      entry["topics"] == log["topics"] && entry["data"] == log["data"]
  end
end