diff --git a/app/jobs/import_bitcoin_utxo_job.rb b/app/jobs/import_bitcoin_utxo_job.rb new file mode 100644 index 000000000..595daedf2 --- /dev/null +++ b/app/jobs/import_bitcoin_utxo_job.rb @@ -0,0 +1,110 @@ +class ImportBitcoinUtxoJob < ApplicationJob + queue_as :bitcoin + + def perform(txid, out_index, cell_id) + ApplicationRecord.transaction do + cell_output = CellOutput.find_by(id: cell_id) + unless cell_output + raise ArgumentError, "Missing cell_output(#{cell_id}), txid(#{txid}), out_index(#{out_index})" + end + + vout_attributes = [] + + # build bitcoin transaction + raw_tx = fetch_raw_transaction(txid) + tx = build_tranaction!(raw_tx) + + # build op_returns + op_returns = build_op_returns!(raw_tx, tx, cell_output.ckb_transaction, vout_attributes) + vout_attributes.concat(op_returns) if op_returns.present? + + # build vout + vout_attributes << build_vout!(raw_tx, tx, out_index, cell_output) + + BitcoinVout.upsert_all(vout_attributes, unique_by: %i[bitcoin_transaction_id index]) if vout_attributes.present? + end + end + + def build_tranaction!(raw_tx) + tx = BitcoinTransaction.find_by(txid: raw_tx["txid"]) + return tx if tx + + # avoid making multiple RPC requests + block_header = rpc.getblockheader(raw_tx["blockhash"]) + BitcoinTransaction.create!( + txid: raw_tx["txid"], + tx_hash: raw_tx["hash"], + time: raw_tx["time"], + block_hash: raw_tx["blockhash"], + block_height: block_header["height"], + ) + end + + def build_op_returns!(raw_tx, tx, ckb_tx, v_attributes) + op_returns = [] + + raw_tx["vout"].each do |vout| + data = vout.dig("scriptPubKey", "hex") + script_pubkey = Bitcoin::Script.parse_from_payload(data.htb) + next unless script_pubkey.op_return? + + op_return = { + bitcoin_transaction_id: tx.id, + bitcoin_address_id: nil, + data:, + index: vout.dig("n"), + asm: vout.dig("scriptPubKey", "asm"), + op_return: true, + ckb_transaction_id: ckb_tx.id, + cell_output_id: nil, + address_id: nil, + } + + op_returns << op_return if v_attributes.exclude?(op_return) + end + + op_returns + end + + def build_vout!(raw_tx, tx, out_index, cell_output) + vout = raw_tx["vout"].find { _1["n"] == out_index } + raise ArgumentError, "Missing vout txid: #{raw_tx['txid']} index: #{out_index}" unless vout + + address_hash = vout.dig("scriptPubKey", "address") + raise ArgumentError, "Missing vout address: #{raw_tx['txid']} index: #{out_index}" unless address_hash + + address = build_address!(address_hash, cell_output) + { + bitcoin_transaction_id: tx.id, + bitcoin_address_id: address.id, + data: vout.dig("scriptPubKey", "hex"), + index: vout.dig("n"), + asm: vout.dig("scriptPubKey", "asm"), + op_return: false, + ckb_transaction_id: cell_output.ckb_transaction_id, + cell_output_id: cell_output.id, + address_id: cell_output.address_id, + } + end + + def build_address!(address_hash, cell_output) + bitcoin_address = BitcoinAddress.find_or_create_by!(address_hash:) + BitcoinAddressMapping. + create_with(bitcoin_address_id: bitcoin_address.id). + find_or_create_by!(ckb_address_id: cell_output.address_id) + + bitcoin_address + end + + def fetch_raw_transaction(txid) + tx_json = Kredis.json txid, expires_in: 1.hour + if tx_json.value.blank? + tx_json.value = rpc.getrawtransaction(txid, 2) + end + tx_json.value + end + + def rpc + @rpc ||= Bitcoin::Rpc.instance + end +end diff --git a/app/models/ckb_sync/api.rb b/app/models/ckb_sync/api.rb index 14c0c6b71..474746bb6 100644 --- a/app/models/ckb_sync/api.rb +++ b/app/models/ckb_sync/api.rb @@ -99,6 +99,10 @@ def xudt_code_hash Settings.xudt_code_hash end + def rgbpp_code_hash + Settings.rgbpp_code_hash + end + METHOD_NAMES.each do |name| define_method name do |*params| call_rpc(name, params:) diff --git a/app/utils/ckb_utils.rb b/app/utils/ckb_utils.rb index 6a26c839c..0ac0d2acf 100644 --- a/app/utils/ckb_utils.rb +++ b/app/utils/ckb_utils.rb @@ -644,8 +644,18 @@ def self.parse_omiga_inscription_data(hex_data) { mint_limit: } end - def self.parse_rgb_args(_args) - # TODO - ["15dede3b31ed87bb6b1d668222127a7b308c1beb6fe99bf4a3f076bcae8e93fe", 4] + def self.is_rgbpp_lock_cell?(lock_script) + lock_script.code_hash == CkbSync::Api.instance.rgbpp_code_hash && lock_script.hash_type == "type" + end + + # * https://learnmeabitcoin.com/technical/general/byte-order/ + # Whenever you're working with transaction/block hashes internally (e.g. inside raw bitcoin data), you use the natural byte order. + # Whenever you're displaying or searching for transaction/block hashes, you use the reverse byte order. + def self.parse_rgbpp_args(args) + args = args.delete_prefix("0x") + out_index = [args[0..7]].pack("H*").unpack1("v") + txid = args[8..-1].scan(/../).reverse.join + + [txid, out_index] end end diff --git a/app/workers/bitcoin_transaction_detect_worker.rb b/app/workers/bitcoin_transaction_detect_worker.rb index 53dce2a34..cd58abd29 100644 --- a/app/workers/bitcoin_transaction_detect_worker.rb +++ b/app/workers/bitcoin_transaction_detect_worker.rb @@ -1,106 +1,57 @@ -class BitcoinTransactionDetectWorker +class BitcoinUtxoDetectWorker include Sidekiq::Worker sidekiq_options queue: "bitcoin" - INIT_BITCOIN_BLOCK_HEIGHT = 100_000 - - attr_accessor :block - def perform(block_id) @block = Block.find_by(id: block_id) return unless @block ApplicationRecord.transaction do - vout_attributes = [] - - transacitons = @block.ckb_transactions.limit(min_transactions_count) - transacitons.each_with_index do |transaction, index| - next if transaction.bitcoin_vouts.exists? - - txid = bitcoin_txids[index] - raw_transaction = rpc.getrawtransaction(txid, 2) - bitcoin_transaction = build_tranaction!(raw_transaction) - - cell_output = transaction.cell_outputs.first - vout_attribute = build_vout_attributes!(raw_transaction, bitcoin_transaction, cell_output) - next unless vout_attribute - - vout_attributes << vout_attribute + @block.ckb_transactions.each do |transaction| + vin_attributes = [] + + # import cell_inputs utxo + transaction.cell_inputs.each do |cell| + previous_cell_output = cell.previous_cell_output + next unless previous_cell_output + + lock_script = previous_cell_output.lock_script + next unless CkbUtils2.is_rgbpp_lock_cell?(lock_script) + + # import previous bitcoin transaction if prev vout is missing + import_utxo!(lock_script.args, previous_cell_output.id, transaction.id) + + previous_vout = BitcoinVout.find_by(cell_output_id: previous_cell_output.id) + vin_attributes << { + previous_bitcoin_vout_id: previous_vout.id, + ckb_transaction_id: transaction.id, + cell_input_id: cell.id, + } + end + + if vin_attributes.present? + BitcoinVin.upsert_all(vin_attributes, unique_by: %i[ckb_transaction_id cell_input_id]) + end + + # import cell_outputs utxo + transaction.cell_outputs.each do |cell| + lock_script = cell.lock_script + next unless CkbUtils2.is_rgbpp_lock_cell?(lock_script) + + import_utxo!(lock_script.args, cell.id, transaction.id) + end end - - return if vout_attributes.blank? - - BitcoinVout.upsert_all(vout_attributes, unique_by: %i[bitcoin_transaction_id index]) end end - private - - def bitcoin_txids - return @txids if @txids - - block_hash = rpc.getblockhash(bitcoin_block_height) - # verbose set to 1 for JSON object - block = rpc.getblock(block_hash, 1) - @txids = block["tx"] - end + def import_utxo!(args, cell_id, _tx_id) + txid, out_index = CkbUtils2.parse_rgbpp_args(args) - def bitcoin_block_height - transaction = BitcoinTransaction.last - transaction ? transaction.block_height + 1 : 100_000 - end - - def min_transactions_count - [block.ckb_transactions_count, bitcoin_txids.count].min - end - - def build_tranaction!(raw_tx) - tx = BitcoinTransaction.find_by(txid: raw_tx["txid"]) - return tx if tx - - # avoid making multiple RPC requests - block_header = rpc.getblockheader(raw_tx["blockhash"]) - BitcoinTransaction.create!( - txid: raw_tx["txid"], - tx_hash: raw_tx["hash"], - time: raw_tx["time"], - block_hash: raw_tx["blockhash"], - block_height: block_header["height"], - ) - end - - def build_vout_attributes!(raw_tx, tx, cell_output) - vout = raw_tx["vout"].find { _1["n"] == cell_output.cell_index } - vout ||= raw_tx["vout"][0] - - address_hash = vout.dig("scriptPubKey", "address") - return unless address_hash - - bitcoin_address = build_address!(address_hash, cell_output) - - { - bitcoin_transaction_id: tx.id, - bitcoin_address_id: bitcoin_address.id, - data: "6a24aa21a9ed5e53af6963d02d7fcf87695798a0715951bd03fb05f524015d88324636141f42", - index: vout.dig("scriptPubKey", "n"), - asm: "OP_RETURN aa21a9ed5e53af6963d02d7fcf87695798a0715951bd03fb05f524015d88324636141f42", - op_return: true, - ckb_transaction_id: cell_output.ckb_transaction_id, - cell_output_id: cell_output.id, - address_id: cell_output.address_id, - } - end - - def build_address!(address_hash, cell_output) - bitcoin_address = BitcoinAddress.find_or_create_by(address_hash:) - BitcoinAddressMapping. - create_with(bitcoin_address_id: bitcoin_address.id). - find_or_create_by!(ckb_address_id: cell_output.address_id) - - bitcoin_address - end - - def rpc - @rpc ||= Bitcoin::Rpc.instance + unless BitcoinTransaction.includes(:bitcoin_vouts).where( + bitcoin_transactions: { txid: }, + bitcoin_vouts: { index: out_index, cell_output_id: cell_id }, + ).exists? + ImportBitcoinUtxoJob.perform_now(txid, out_index, cell_id) + end end end diff --git a/config/settings.mainnet.yml b/config/settings.mainnet.yml index 8ebb71243..97bc45746 100644 --- a/config/settings.mainnet.yml +++ b/config/settings.mainnet.yml @@ -71,3 +71,6 @@ type_id_code_hash: "0x0000000000000000000000000000000000000000000000000054595045 homepage_transactions_records_count: 15 homepage_block_records_count: 15 proposal_window: 10 + +# rgbpp code hash +rgbpp_code_hash: "0x0000000000000000000000000000000000000000000000000000000000000000" diff --git a/config/settings.testnet.yml b/config/settings.testnet.yml index c129a5f5a..230570c38 100644 --- a/config/settings.testnet.yml +++ b/config/settings.testnet.yml @@ -74,3 +74,6 @@ type_id_code_hash: "0x0000000000000000000000000000000000000000000000000054595045 homepage_transactions_records_count: 15 homepage_block_records_count: 15 proposal_window: 10 + +# rgbpp code hash +rgbpp_code_hash: "0xaddb21cf401d1a609e60198f850c4417ff03f98462ba4a3f101fa2741f481228"