diff --git a/app/controllers/api/v1/xudts_controller.rb b/app/controllers/api/v1/xudts_controller.rb index 76ef3e728..5c7ee86bb 100644 --- a/app/controllers/api/v1/xudts_controller.rb +++ b/app/controllers/api/v1/xudts_controller.rb @@ -55,7 +55,7 @@ def download_csv end def snapshot - args = params.permit(:id, :number) + args = params.permit(:id, :number, :merge_with_owner) file = CsvExportable::ExportUdtSnapshotJob.perform_now(args.to_h) if params[:format] == "json" diff --git a/app/jobs/csv_exportable/base_exporter.rb b/app/jobs/csv_exportable/base_exporter.rb index e66bd91f3..b3580aa28 100644 --- a/app/jobs/csv_exportable/base_exporter.rb +++ b/app/jobs/csv_exportable/base_exporter.rb @@ -55,11 +55,11 @@ def parse_udt_amount(amount, decimal) result = amount_big_decimal / (BigDecimal(10)**decimal_int) if decimal_int > 20 - return "#{result.round(20).to_s('F')}..." + return "#{format('%.2f', result.round(20))}..." end if result.to_s.length >= 16 || result < BigDecimal("0.000001") - return result.round(decimal_int).to_s("F") + return format("%.#{decimal_int}f", result.round(decimal_int)) end result.to_s("F") diff --git a/app/jobs/csv_exportable/export_udt_snapshot_job.rb b/app/jobs/csv_exportable/export_udt_snapshot_job.rb index 252fe9a7c..bc8b9801e 100644 --- a/app/jobs/csv_exportable/export_udt_snapshot_job.rb +++ b/app/jobs/csv_exportable/export_udt_snapshot_job.rb @@ -3,68 +3,86 @@ class ExportUdtSnapshotJob < BaseExporter attr_accessor :udt, :block def perform(args) + find_block_and_udt(args) + type_script = TypeScript.find_by(@udt.type_script) + + cell_outputs = fetch_cell_outputs(type_script.id) + data = fetch_address_data(cell_outputs) + merged_data = merge_data(data, to_boolean(args[:merge_with_owner])) + + header = generate_header(to_boolean(args[:merge_with_owner])) + rows = prepare_rows(merged_data) + + generate_csv(header, rows) + end + + private + + def find_block_and_udt(args) @block = Block.find_by!(number: args[:number]) @udt = Udt.published_xudt.find_by!(type_hash: args[:id]) - type_script = TypeScript.find_by(@udt.type_script) + end + def fetch_cell_outputs(type_script_id) condition = <<-SQL - type_script_id = #{type_script.id} AND + type_script_id = #{type_script_id} AND block_timestamp <= #{@block.timestamp} AND (consumed_block_timestamp > #{@block.timestamp} OR consumed_block_timestamp IS NULL) SQL cell_outputs = CellOutput.where(condition).group(:address_id).sum(:udt_amount) - cell_outputs = cell_outputs.reject { |_, v| v.to_f.zero? } + cell_outputs.reject { |_, v| v.to_f.zero? } + end - data = [] - cell_outputs.keys.each_slice(1000) do |address_ids| + def fetch_address_data(cell_outputs) + cell_outputs.keys.each_slice(1000).flat_map do |address_ids| addresses = Address.includes(bitcoin_address_mapping: [:bitcoin_address]). where(id: address_ids).pluck("addresses.id", "addresses.address_hash", "bitcoin_addresses.address_hash") - addresses.each do |address| - data << { + addresses.map do |address| + { address_hash: address[1], bitcoin_address_hash: address[2], udt_amount: cell_outputs[address[0]], } end end + end - rows = [] - data.sort_by { |item| -item[:udt_amount] }.each do |item| - row = generate_row(item) - next if row.blank? - - rows << row + def merge_data(data, merge_with_owner) + data.each_with_object(Hash.new(0)) do |entry, hash| + owner = merge_with_owner ? (entry[:bitcoin_address_hash].presence || entry[:address_hash]) : entry[:address_hash] + hash[owner] += entry[:udt_amount] end + end - header = ["Token Symbol", "Block Height", "UnixTimestamp", "date(UTC)", "Owner", "CKB Address", "Amount"] - generate_csv(header, rows) + def prepare_rows(merged_data) + merged_data.sort_by { |_, amount| -amount }.map { |item| generate_row(item) }.compact end def generate_row(item) datetime = datetime_utc(@block.timestamp) + decimal = @udt.decimal - if (decimal = @udt.decimal) - [ - @udt.symbol, - @block.number, - @block.timestamp, - datetime, - item[:bitcoin_address_hash] || item[:address_hash], - item[:address_hash], - parse_udt_amount(item[:udt_amount].to_d, decimal), - ] + [ + @udt.symbol, + @block.number, + @block.timestamp, + datetime, + item[0], + decimal.present? ? parse_udt_amount(item[1].to_d, decimal) : "#{item[1]} (raw)", + ] + end + + def generate_header(merge_with_owner) + if merge_with_owner + ["Token Symbol", "Block Height", "UnixTimestamp", "date(UTC)", "Owner", "Amount"] else - [ - @udt.symbol, - @block.number, - @block.timestamp, - datetime, - item[:bitcoin_address_hash] || item[:address_hash], - item[:address_hash], - "#{item[:udt_amount]} (raw)", - ] + ["Token Symbol", "Block Height", "UnixTimestamp", "date(UTC)", "CKB Address", "Amount"] end end + + def to_boolean(param) + param == true || param.to_s.downcase == "true" || param.to_s == "1" + end end -end \ No newline at end of file +end