diff --git a/.env.example b/.env.example index 9b1ffa5dc..fe6ed13b7 100644 --- a/.env.example +++ b/.env.example @@ -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*/" diff --git a/app/controllers/api/v2/bitcoin_transactions_controller.rb b/app/controllers/api/v2/bitcoin_transactions_controller.rb index 252866eef..95b2bf631 100644 --- a/app/controllers/api/v2/bitcoin_transactions_controller.rb +++ b/app/controllers/api/v2/bitcoin_transactions_controller.rb @@ -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") @@ -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 diff --git a/app/services/bitcoin/rpc.rb b/app/services/bitcoin/rpc.rb index 264d5a97d..cb330b069 100644 --- a/app/services/bitcoin/rpc.rb +++ b/app/services/bitcoin/rpc.rb @@ -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| @@ -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 diff --git a/app/workers/bitcoin_transaction_detect_worker.rb b/app/workers/bitcoin_transaction_detect_worker.rb index 3512ae3f6..fd48d64c8 100644 --- a/app/workers/bitcoin_transaction_detect_worker.rb +++ b/app/workers/bitcoin_transaction_detect_worker.rb @@ -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")