From c5bcfe825169317937f32863c7291f49106e3077 Mon Sep 17 00:00:00 2001 From: Rabbit Date: Mon, 15 Jan 2024 10:41:07 +0800 Subject: [PATCH] refactor: transaction details --- .rubocop.yml | 4 +- .../api/v2/ckb_transactions_controller.rb | 89 +---------- app/controllers/concerns/.keep | 0 .../concerns/cell_data_comparator.rb | 151 ++++++++++++++++++ 4 files changed, 159 insertions(+), 85 deletions(-) delete mode 100644 app/controllers/concerns/.keep create mode 100644 app/controllers/concerns/cell_data_comparator.rb diff --git a/.rubocop.yml b/.rubocop.yml index 94304388b5..d8245286d0 100755 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -449,9 +449,9 @@ Layout/InitialIndentation: Enabled: false Layout/LineLength: - Description: 'Limit lines to 80 characters.' + Description: 'Limit lines to 160 characters.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits' - Max: 80 + Max: 160 # Lint diff --git a/app/controllers/api/v2/ckb_transactions_controller.rb b/app/controllers/api/v2/ckb_transactions_controller.rb index 382b40b282..eab1070ca2 100644 --- a/app/controllers/api/v2/ckb_transactions_controller.rb +++ b/app/controllers/api/v2/ckb_transactions_controller.rb @@ -1,17 +1,16 @@ module Api module V2 class CkbTransactionsController < BaseController + include CellDataComparator + before_action :set_page_and_page_size, only: %i[display_inputs display_outputs] def details - ckb_transaction = CkbTransaction.where(tx_hash: params[:id]).order(tx_status: :desc).first + ckb_transaction = CkbTransaction.find_by(tx_hash: params[:id]) head :not_found and return if ckb_transaction.blank? expires_in 10.seconds, public: true, must_revalidate: true - - input_capacities = build_cell_capacities(ckb_transaction.display_inputs) - output_capacities = build_cell_capacities(ckb_transaction.display_outputs) - transfers = build_transfers(input_capacities, output_capacities) + transfers = compare_cells(ckb_transaction) render json: { data: transfers } end @@ -19,7 +18,7 @@ def details def display_inputs expires_in 1.hour, public: true, must_revalidate: true - ckb_transaction = CkbTransaction.where(tx_hash: params[:id]).order(tx_status: :desc).first + ckb_transaction = CkbTransaction.find_by(tx_hash: params[:id]) head :not_found and return if ckb_transaction.blank? if ckb_transaction.is_cellbase @@ -44,7 +43,7 @@ def display_inputs def display_outputs expires_in 1.hour, public: true, must_revalidate: true - ckb_transaction = CkbTransaction.where(tx_hash: params[:id]).order(tx_status: :desc).first + ckb_transaction = CkbTransaction.find_by(tx_hash: params[:id]) head :not_found and return if ckb_transaction.blank? if ckb_transaction.is_cellbase @@ -67,82 +66,6 @@ def display_outputs private - def build_cell_capacities(outputs) - cell_capacities = Hash.new { |hash, key| hash[key] = {} } - outputs.each do |output| - parsed_output = JSON.parse(output.to_json, object_class: OpenStruct) - next if parsed_output.from_cellbase - - unit = token_unit(parsed_output) - address = parsed_output.address_hash - udt_info = parsed_output.udt_info - - if (cell_capacity = cell_capacities[[address, unit]]).blank? - cell_capacity = { - capacity: parsed_output.capacity.to_f, - cell_type: parsed_output.cell_type, - udt_info: { - symbol: udt_info&.symbol, - amount: udt_info&.amount.to_f, - decimal: udt_info&.decimal, - type_hash: udt_info&.type_hash, - published: !!udt_info&.published, - display_name: udt_info&.display_name, - uan: udt_info&.uan - }, - m_nft_info: parsed_output.m_nft_info.to_h - } - else - cell_capacity[:capacity] += parsed_output.capacity.to_f - cell_capacity[:udt_info][:amount] += udt_info.amount.to_f unless unit == "CKB" - end - - cell_capacities[[address, unit]] = cell_capacity - end - - cell_capacities - end - - def build_transfers(input_capacities, output_capacities) - capacities = Hash.new { |hash, key| hash[key] = [] } - keys = input_capacities.keys | output_capacities.keys - keys.each do |key| - address_hash, unit = key - input = input_capacities[key] - output = output_capacities[key] - - # There may be keys in both input_capacities and output_capacities that do not exist - cell_type = output[:cell_type] || input[:cell_type] - capacity_change = output[:capacity].to_f - input[:capacity].to_f - m_nft_info = output[:m_nft_info] || input[:m_nft_info] - - transfer = { capacity: capacity_change, cell_type: cell_type } - transfer[:m_nft_info] = m_nft_info if m_nft_info.present? - - if unit != "CKB" - output_amount = output[:udt_info] ? output[:udt_info][:amount] : 0.0 - input_amount = input[:udt_info] ? input[:udt_info][:amount] : 0.0 - amount_change = output_amount - input_amount - transfer[:udt_info] = output[:udt_info] || input[:udt_info] - transfer[:udt_info][:amount] = amount_change - end - - capacities[address_hash] << CkbUtils.hash_value_to_s(transfer) - end - - capacities.map do |address, value| - { address: address, transfers: value } - end - end - - def token_unit(cell) - if (udt_info = cell.udt_info).present? - udt_info.type_hash - else - "CKB" - end - end - def set_page_and_page_size @page = params.fetch(:page, 1) @page_size = params.fetch(:page_size, CkbTransaction.default_per_page) diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/app/controllers/concerns/cell_data_comparator.rb b/app/controllers/concerns/cell_data_comparator.rb new file mode 100644 index 0000000000..a575995519 --- /dev/null +++ b/app/controllers/concerns/cell_data_comparator.rb @@ -0,0 +1,151 @@ +module CellDataComparator + extend ActiveSupport::Concern + + private + + def compare_cells(transaction) + inputs = transaction.input_cells + outputs = transaction.cell_outputs + normal_transfers = diff_normal_cells(inputs, outputs) + udt_transfers = diff_udt_cells(inputs, outputs) + cota_nft_transfers = diff_cota_nft_cells(transaction, inputs, outputs) + normal_nft_transfers = diff_normal_nft_cells(inputs, outputs) + nft_class_transfers = diff_nft_capacities(inputs, outputs) + + merged_transfers = + [normal_transfers, udt_transfers, cota_nft_transfers, normal_nft_transfers, nft_class_transfers].reduce do |acc, h| + acc.merge(h) { |_, ov, nv| ov + nv } + end + + merged_transfers.map do |address_id, transfers| + address = Address.find_by(id: address_id) + { address: address.address_hash, transfers: } + end + end + + def diff_normal_cells(inputs, outputs) + transfers = Hash.new { |h, k| h[k] = Array.new } + inputs = inputs.normal.group(:address_id).sum(:capacity) + outputs = outputs.normal.group(:address_id).sum(:capacity) + + (inputs.keys | outputs.keys).each do |k| + capacity = outputs[k].to_f - inputs[k].to_f + transfers[k] << CkbUtils.hash_value_to_s({ capacity:, cell_type: "normal" }) + end + + transfers + end + + def diff_udt_cells(inputs, outputs) + transfers = Hash.new { |h, k| h[k] = Array.new } + udt_infos = Hash.new { |h, k| h[k] = nil } + + process_udt = ->(c, h) { + info = Udt.find_by(type_hash: c.type_hash, published: true) + unless udt_infos[c.type_hash] + udt_infos[c.type_hash] = { + symbol: info&.symbol, + decimal: info&.decimal, + display_name: info&.display_name, + uan: info&.uan, + } + end + + k = [c.address_id, c.type_hash] + h[k] ||= { capacity: 0.0, amount: 0.0 } + h[k][:capacity] += c.capacity + h[k][:amount] += c.udt_amount + } + inputs = inputs.udt.each_with_object({}) { |c, h| process_udt.call(c, h) } + outputs = outputs.udt.each_with_object({}) { |c, h| process_udt.call(c, h) } + + (inputs.keys | outputs.keys).each do |k| + input = inputs[k] + output = outputs[k] + + amount = output&.dig(:amount).to_f - input&.dig(:amount).to_f + capacity = output&.dig(:capacity).to_f - input&.dig(:capacity).to_f + udt_info = udt_infos[k[1]].merge(amount:) + transfers[k[0]] << CkbUtils.hash_value_to_s({ capacity:, cell_type: "udt", udt_info: }) + end + + transfers + end + + def diff_cota_nft_cells(transaction, inputs, outputs) + transfers = Hash.new { |h, k| h[k] = Array.new } + inputs = inputs.cota_regular.group(:address_id).sum(:capacity) + outputs = outputs.cota_regular.group(:address_id).sum(:capacity) + + (inputs.keys | outputs.keys).each do |k| + capacity = outputs[k].to_f - inputs[k].to_f + transfers[k] << { capacity: capacity.to_s, cell_type: "cota_regular", cota_info: cota_info(transaction, k) } + end + + transfers + end + + def diff_normal_nft_cells(inputs, outputs) + transfers = Hash.new { |h, k| h[k] = Array.new } + cell_types = %w(m_nft_token nrc_721_token spore_cell) + + process_nft = ->(c, h) { + k = [c.address_id, c.cell_type, c.type_hash] + h[k] ||= { capacity: 0.0, count: 0 } + h[k][:capacity] += c.capacity + h[k][:count] -= 1 + } + inputs = inputs.where(cell_type: cell_types).each_with_object({}) { |c, h| process_nft.call(c, h) } + outputs = outputs.where(cell_type: cell_types).each_with_object({}) { |c, h| process_nft.call(c, h) } + + (inputs.keys | outputs.keys).each do |k| + address_id, cell_type, type_hash = k + token_id, collection_name = token_info(type_hash) + input = inputs[k] + output = outputs[k] + capacity = output&.dig(:capacity).to_f - input&.dig(:capacity).to_f + count = output&.dig(:count).to_i + input&.dig(:count).to_i + transfers[address_id] << CkbUtils.hash_value_to_s({ capacity:, cell_type:, token_id:, collection_name:, count: }) + end + + transfers + end + + def diff_nft_capacities(inputs, outputs) + transfers = Hash.new { |h, k| h[k] = Array.new } + cell_types = %w(m_nft_issuer m_nft_class nrc_721_factory cota_registry spore_cluster) + inputs = inputs.where(cell_type: cell_types).group(:address_id, :cell_type).sum(:capacity) + outputs = outputs.where(cell_type: cell_types).group(:address_id, :cell_type).sum(:capacity) + + (inputs.keys | outputs.keys).each do |k| + capacity = outputs[k].to_f - inputs[k].to_f + transfers[k[0]] << CkbUtils.hash_value_to_s({ capacity:, cell_type: k[1] }) + end + + transfers + end + + def token_info(script_hash) + item = TokenItem.joins(:type_script).where(type_script: { script_hash: }).take + [item&.token_id, item&.collection&.name] + end + + def cota_info(transaction, address_id) + info = Array.new + process_transfer = ->(item, count) { + collection = item.collection + info << CkbUtils.hash_value_to_s({ + collection_name: collection.name, + count:, + token_id: item.token_id, + }) + } + + transaction.token_transfers.each do |t| + process_transfer.call(t.item, -1) if t.from_id == address_id + process_transfer.call(t.item, 1) if t.to_id == address_id + end + + info + end +end