Skip to content

Commit

Permalink
Issue 700 (#1966)
Browse files Browse the repository at this point in the history
* feat: add bitcoin signet

* feat: batch get bitcoin raw transactions
  • Loading branch information
rabbitz authored Jun 18, 2024
1 parent 2d311a2 commit 1955f64
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 25 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ SECRET_KEY_BASE=""

# -------------------------------- Bitcoin segment --------------------------------
BITCOIN_NODE_URL=""
BITCOIN_SIGNET_NODE_URL=""
BITCOIN_SIGNET_USER=""
BITCOIN_SIGNET_PASS=""

# Dynamic CORS configuration
PARTNER_DOMAINS="/localhost:\d*/"
16 changes: 5 additions & 11 deletions app/controllers/api/v2/bitcoin_transactions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ def query
not_cached = cache_keys - res.keys
to_cache = {}

raw_transactions = ->(txids) do
Bitcoin::Rpc.instance.batch_get_raw_transactions(txids)
end

if not_cached.present?
get_raw_transactions(not_cached).each do |tx|
raw_transactions.call(not_cached).each do |tx|
next if tx.dig("error").present?

txid = tx.dig("result", "txid")
Expand All @@ -25,16 +29,6 @@ def query
Rails.logger.error "get raw transactions(#{params[:txids]}) failed: #{e.message}"
render json: {}, status: :not_found
end

private

def get_raw_transactions(txids)
payload = txids.map.with_index do |txid, index|
{ jsonrpc: "1.0", id: index + 1, method: "getrawtransaction", params: [txid, 2] }
end
response = HTTP.timeout(10).post(ENV["BITCOIN_NODE_URL"], json: payload)
JSON.parse(response.to_s)
end
end
end
end
88 changes: 81 additions & 7 deletions app/services/bitcoin/rpc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ module Bitcoin
class Rpc
include Singleton

METHOD_NAMES = %w(getchaintips getrawtransaction getblock getblockhash getblockheader getblockchaininfo)
def initialize(endpoint = ENV["BITCOIN_NODE_URL"])
@endpoint = endpoint
METHOD_NAMES = %w(getchaintips getrawtransaction getblock getblockhash getblockheader getblockchaininfo).freeze
SIGNET_WHITELISTED_METHODS = %w(getrawtransaction).freeze

def initialize
@id = 0
@endpoint = ENV["BITCOIN_NODE_URL"]
@signet_endpoint = ENV["BITCOIN_SIGNET_NODE_URL"]
@signet_user = ENV["BITCOIN_SIGNET_USER"]
@signet_pass = ENV["BITCOIN_SIGNET_PASS"]
end

METHOD_NAMES.each do |name|
Expand All @@ -14,18 +19,87 @@ def initialize(endpoint = ENV["BITCOIN_NODE_URL"])
end
end

def batch_get_raw_transactions(txids)
payload_generator = Proc.new { |txid, index| { jsonrpc: "1.0", id: index + 1, method: "getrawtransaction", params: [txid, 2] } }
payload = txids.map.with_index(&payload_generator)

if mainnet_mode?
make_request(@endpoint, payload)
else
signet_response = make_signet_request(payload, "getrawtransaction")
return make_request(@endpoint, payload) if signet_response.blank?

consolidate_responses(signet_response, txids, payload_generator)
end
end

private

def call_rpc(method, params: [])
@id += 1
payload = { jsonrpc: "1.0", id: @id, method:, params: }
response = HTTP.timeout(10).post(@endpoint, json: payload)

if mainnet_mode?
make_request(@endpoint, payload)
else
make_signet_request(payload, method) || make_request(@endpoint, payload)
end
end

def mainnet_mode?
CkbSync::Api.instance.mode == CKB::MODE::MAINNET
end

def make_request(endpoint, payload)
response = HTTP.timeout(60).post(endpoint, json: payload)
parse_response(response)
end

def make_signet_request(payload, method)
return unless SIGNET_WHITELISTED_METHODS.include?(method)

begin
response = HTTP.basic_auth(user: @signet_user, pass: @signet_pass).timeout(60).post(@signet_endpoint, json: payload)
parse_response(response)
rescue StandardError => e
Rails.logger.error("Error making signet request: #{e.message}")
nil # Return nil if the request fails, allowing fallback to the main testnet endpoint
end
end

def parse_response(response)
data = JSON.parse(response.to_s)
if (err = data["error"]).present?
raise ArgumentError, err["message"]

return data if data.is_a?(Array)

if data.is_a?(Hash)
raise ArgumentError, data["error"]["message"] if data["error"].present?
else
data
raise ArgumentError, "Unexpected response format: #{data.class}"
end

data
end

def consolidate_responses(signet_response, txids, payload_generator)
consolidated_response = []
fetched_txids = []

signet_response.each do |response|
if response["result"].present?
fetched_txids << response["result"]["txid"]
consolidated_response << response
end
end

unfetched_txids = txids - fetched_txids

if unfetched_txids.present?
unfetched_payload = unfetched_txids.map.with_index(&payload_generator)
consolidated_response.concat(make_request(@endpoint, unfetched_payload))
end

consolidated_response
end
end
end
10 changes: 3 additions & 7 deletions app/workers/bitcoin_transaction_detect_worker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,15 @@ def collect_rgb_ids(cell_output)
def cache_raw_transactions!
return if @txids.empty?

get_raw_transactions = ->(txids) do
payload = txids.map.with_index do |txid, index|
{ jsonrpc: "1.0", id: index + 1, method: "getrawtransaction", params: [txid, 2] }
end
response = HTTP.timeout(10).post(ENV["BITCOIN_NODE_URL"], json: payload)
JSON.parse(response.to_s)
raw_transactions = ->(txids) do
Bitcoin::Rpc.instance.batch_get_raw_transactions(txids)
end

to_cache = {}
not_cached = @txids.uniq.reject { Rails.cache.exist?(_1) }

not_cached.each_slice(BITCOIN_RPC_BATCH_SIZE).each do |txids|
get_raw_transactions.call(txids).each do |data|
raw_transactions.call(txids).each do |data|
next if data && data["error"].present?

txid = data.dig("result", "txid")
Expand Down

0 comments on commit 1955f64

Please sign in to comment.