diff --git a/app/controllers/api/v1/udts_controller.rb b/app/controllers/api/v1/udts_controller.rb index c62e617e0..fdac9405b 100644 --- a/app/controllers/api/v1/udts_controller.rb +++ b/app/controllers/api/v1/udts_controller.rb @@ -1,7 +1,7 @@ module Api module V1 class UdtsController < ApplicationController - before_action :validate_query_params, only: :show + before_action :validate_query_params, only: %i[show holder_allocation] before_action :validate_pagination_params, :pagination_params, only: :index def index @@ -84,6 +84,24 @@ def download_csv raise Api::V1::Exceptions::UdtNotFoundError end + def holder_allocation + udt = Udt.find_by!(type_hash: params[:id], published: true) + holder_allocation = udt.udt_holder_allocations.find_by(contract_id: nil) + btc_holder_count = holder_allocation&.btc_holder_count || 0 + + lock_hashes = udt.udt_holder_allocations.includes(:contract).where.not(contract_id: nil).map do |allocation| + { + name: allocation.contract.name, + code_hash: allocation.contract.code_hash, + holder_count: allocation.ckb_holder_count, + } + end + + render json: { btc_holder_count:, lock_hashes: } + rescue ActiveRecord::RecordNotFound + raise Api::V1::Exceptions::UdtNotFoundError + end + private def validate_query_params diff --git a/app/controllers/api/v2/nft/collections_controller.rb b/app/controllers/api/v2/nft/collections_controller.rb index 92c5139a3..b74b12dd0 100644 --- a/app/controllers/api/v2/nft/collections_controller.rb +++ b/app/controllers/api/v2/nft/collections_controller.rb @@ -7,6 +7,10 @@ def index if params[:type].present? scope = scope.where(standard: params[:type]) end + if params[:tags].present? + tags = parse_tags + scope = scope.where("tags @> array[?]::varchar[]", tags) unless tags.empty? + end pagy, collections = pagy(sort_collections(scope)) render json: { @@ -51,6 +55,11 @@ def sort_collections(records) records.order("#{sort} #{order}") end end + + def parse_tags + tags = params[:tags].split(",") + tags & TokenCollection::VALID_TAGS + end end end end diff --git a/app/controllers/concerns/cell_data_comparator.rb b/app/controllers/concerns/cell_data_comparator.rb index d5f39a40a..65e35b99c 100644 --- a/app/controllers/concerns/cell_data_comparator.rb +++ b/app/controllers/concerns/cell_data_comparator.rb @@ -104,7 +104,7 @@ def diff_cota_nft_cells(transaction, inputs, outputs) def diff_normal_nft_cells(inputs, outputs) transfers = Hash.new { |h, k| h[k] = Array.new } nft_infos = Hash.new { |h, k| h[k] = nil } - cell_types = %w(m_nft_token nrc_721_token spore_cell m_nft_issuer + cell_types = %w(m_nft_token nrc_721_token spore_cell did_cell m_nft_issuer m_nft_class nrc_721_factory cota_registry spore_cluster) process_nft = ->(c, h, o) { @@ -136,7 +136,7 @@ def diff_normal_nft_cells(inputs, outputs) def nft_info(cell) case cell.cell_type - when "m_nft_token", "nrc_721_token", "spore_cell" + when "m_nft_token", "nrc_721_token", "spore_cell", "did_cell" item = TokenItem.joins(:type_script).where(type_script: { script_hash: cell.type_hash }).take { token_id: item&.token_id, name: item&.collection&.name } when "m_nft_issuer" diff --git a/app/interactions/rgb_transactions/index.rb b/app/interactions/rgb_transactions/index.rb index 63da084e6..7b4d591b9 100644 --- a/app/interactions/rgb_transactions/index.rb +++ b/app/interactions/rgb_transactions/index.rb @@ -10,11 +10,7 @@ def execute annotations = BitcoinAnnotation.includes(:ckb_transaction). where("bitcoin_annotations.tags @> array[?]::varchar[]", ["rgbpp"]). order(order_by => asc_or_desc) - - if leap_direction.present? - annotations = annotations.where("bitcoin_annotations.leap_direction = ?", leap_direction) - end - + annotations = annotations.where(leap_direction:) if leap_direction.present? annotations.page(page).per(page_size) end diff --git a/app/jobs/import_btc_time_cells_job.rb b/app/jobs/import_btc_time_cells_job.rb new file mode 100644 index 000000000..2b2600704 --- /dev/null +++ b/app/jobs/import_btc_time_cells_job.rb @@ -0,0 +1,90 @@ +class ImportBtcTimeCellsJob < ApplicationJob + queue_as :bitcoin + + def perform(cell_ids) + ApplicationRecord.transaction do + cell_outputs = CellOutput.where(id: cell_ids) + + utxo_map = build_utxo_map(cell_outputs) + raw_tx_data = fetch_raw_transactions!(utxo_map) + transactions = build_transactions!(cell_outputs, raw_tx_data, utxo_map) + + bitcoin_transfers_attributes = [] + + cell_outputs.each do |cell_output| + txid = utxo_map[cell_output.id] + tx = transactions[txid] + + # build transfer + bitcoin_transfers_attributes << { + bitcoin_transaction_id: tx.id, + ckb_transaction_id: cell_output.ckb_transaction_id, + lock_type: "btc_time", + cell_output_id: cell_output.id, + } + rescue StandardError => e + Rails.logger.error("Handle btc time cell (id: #{cell_output.id}) failed: #{e.message}") + end + + if bitcoin_transfers_attributes.present? + BitcoinTransfer.upsert_all(bitcoin_transfers_attributes, + unique_by: %i[cell_output_id]) + end + end + rescue StandardError => e + Rails.logger.error("ImportBtcTimeCells failed: #{e.message}") + end + + def build_utxo_map(cell_outputs) + cell_outputs.each_with_object({}) do |cell_output, data| + parsed_args = CkbUtils.parse_btc_time_lock_cell(cell_output.lock_script.args) + data[cell_output.id] = parsed_args.txid + end + end + + def fetch_raw_transactions!(utxo_map) + txids = utxo_map.values.uniq + + raw_tx_data = {} + txids.each do |txid| + data = Rails.cache.read(txid) + unless data + data = rpc.getrawtransaction(txid, 2) + Rails.cache.write(txid, data, expires_in: 30.minutes) + end + raw_tx_data[txid] = data["result"] + rescue StandardError => e + Rails.logger.error("Failed to fetch transaction #{txid}: #{e.message}") + end + + raw_tx_data + end + + def build_transactions!(cell_outputs, raw_tx_data, utxo_map) + transaction_attributes = [] + + cell_outputs.each do |cell_output| + txid = utxo_map[cell_output.id] + raw_tx = raw_tx_data[txid] + + next unless raw_tx + + created_at = Time.at((cell_output.block_timestamp / 1000).to_i).in_time_zone + transaction_attributes << { + txid: raw_tx["txid"], + tx_hash: raw_tx["hash"], + time: raw_tx["time"], + block_hash: raw_tx["blockhash"], + block_height: 0, + created_at:, + } + end + + BitcoinTransaction.upsert_all(transaction_attributes, unique_by: :txid) + BitcoinTransaction.where(txid: transaction_attributes.map { _1[:txid] }).index_by(&:txid) + end + + def rpc + @rpc ||= Bitcoin::Rpc.instance + end +end diff --git a/app/jobs/import_rgbpp_cells_job.rb b/app/jobs/import_rgbpp_cells_job.rb new file mode 100644 index 000000000..b9762dd8b --- /dev/null +++ b/app/jobs/import_rgbpp_cells_job.rb @@ -0,0 +1,196 @@ +class ImportRgbppCellsJob < ApplicationJob + class MissingVoutError < StandardError; end + class MissingAddressError < StandardError; end + + queue_as :bitcoin + + def perform(cell_ids) + ApplicationRecord.transaction do + cell_outputs = CellOutput.where(id: cell_ids) + + utxo_map = build_utxo_map(cell_outputs) + raw_tx_data = fetch_raw_transactions!(utxo_map) + transactions = build_transactions!(cell_outputs, raw_tx_data, utxo_map) + + vout_attributes = [] + op_returns_attributes = [] + vin_attributes = [] + bitcoin_transfers_attributes = [] + + cell_outputs.each do |cell_output| + utxo = utxo_map[cell_output.id] + txid = utxo[:txid] + out_index = utxo[:out_index] + + raw_tx = raw_tx_data[txid] + tx = transactions[txid] + + next unless raw_tx && tx + + # build op_returns + op_returns = build_op_returns!(raw_tx, tx, cell_output.ckb_transaction) + op_returns_attributes.concat(op_returns) if op_returns.present? + + # build vouts + vout = build_vout!(raw_tx, tx, out_index, cell_output) + vout_attributes << vout if vout.present? + + # build vin + vin = build_vin!(cell_output.id, tx) + vin_attributes << vin if vin.present? + + # build transfer + bitcoin_transfers_attributes << { + bitcoin_transaction_id: tx.id, + ckb_transaction_id: cell_output.ckb_transaction_id, + lock_type: "rgbpp", + cell_output_id: cell_output.id, + } + + rescue StandardError => e + Rails.logger.error("Handle rgbpp cell (id: #{cell_output.id}) failed: #{e.message}") + end + + if vout_attributes.present? + BitcoinVout.upsert_all(vout_attributes, + unique_by: %i[bitcoin_transaction_id index cell_output_id]) + end + + if vin_attributes.present? + BitcoinVin.upsert_all(vin_attributes, + unique_by: %i[ckb_transaction_id cell_input_id]) + end + + if op_returns_attributes.present? + BitcoinVout.upsert_all(op_returns_attributes, + unique_by: %i[bitcoin_transaction_id index cell_output_id]) + end + + if bitcoin_transfers_attributes.present? + BitcoinTransfer.upsert_all(bitcoin_transfers_attributes, + unique_by: %i[cell_output_id]) + end + end + rescue StandardError => e + Rails.logger.error("ImportRgbppCells failed: #{e.message}") + end + + def build_utxo_map(cell_outputs) + cell_outputs.each_with_object({}) do |cell_output, data| + txid, out_index = CkbUtils.parse_rgbpp_args(cell_output.lock_script.args) + data[cell_output.id] = { txid:, out_index: } + end + end + + def fetch_raw_transactions!(utxo_map) + txids = utxo_map.values.map { _1[:txid] }.uniq + + raw_tx_data = {} + txids.each do |txid| + data = Rails.cache.read(txid) + unless data + data = rpc.getrawtransaction(txid, 2) + Rails.cache.write(txid, data, expires_in: 30.minutes) + end + raw_tx_data[txid] = data["result"] + rescue StandardError => e + Rails.logger.error("Failed to fetch transaction #{txid}: #{e.message}") + end + + raw_tx_data + end + + def build_transactions!(cell_outputs, raw_tx_data, utxo_map) + transaction_attributes = [] + + cell_outputs.each do |cell_output| + txid = utxo_map[cell_output.id][:txid] + raw_tx = raw_tx_data[txid] + + next unless raw_tx + + created_at = Time.at((cell_output.block_timestamp / 1000).to_i).in_time_zone + transaction_attributes << { + txid: raw_tx["txid"], + tx_hash: raw_tx["hash"], + time: raw_tx["time"], + block_hash: raw_tx["blockhash"], + block_height: 0, + created_at:, + } + end + + BitcoinTransaction.upsert_all(transaction_attributes, unique_by: :txid) + BitcoinTransaction.where(txid: transaction_attributes.map { _1[:txid] }).index_by(&:txid) + end + + def build_op_returns!(raw_tx, tx, ckb_tx) + raw_tx["vout"].map do |vout| + data = vout.dig("scriptPubKey", "hex") + script_pubkey = Bitcoin::Script.parse_from_payload(data.htb) + next unless script_pubkey.op_return? + + { + bitcoin_transaction_id: tx.id, + data:, + index: vout.dig("n"), + asm: vout.dig("scriptPubKey", "asm"), + op_return: true, + ckb_transaction_id: ckb_tx.id, + } + end.compact + end + + def build_vout!(raw_tx, tx, out_index, cell_output) + vout = raw_tx["vout"].find { _1["n"] == out_index } + raise MissingVoutError, "Missing vout txid: #{raw_tx['txid']} index: #{out_index}" unless vout + + address_hash = vout.dig("scriptPubKey", "address") + raise MissingAddressError, "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) + created_at = Time.at((cell_output.block_timestamp / 1000).to_i).in_time_zone + bitcoin_address = BitcoinAddress.create_with(created_at:).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 build_vin!(cell_id, tx) + cell_input = CellInput.find_by(previous_cell_output_id: cell_id) + previous_vout = BitcoinVout.find_by(cell_output_id: cell_id) + return unless cell_input && previous_vout + + previous_cell_output = cell_input.output + # check whether previous_cell_output utxo consumed + if previous_cell_output.dead? && previous_vout.binding? + previous_vout.update!(status: "normal", consumed_by_id: tx.id) + end + + { + previous_bitcoin_vout_id: previous_vout.id, + ckb_transaction_id: cell_input.ckb_transaction_id, + cell_input_id: cell_input.id, + } + end + + def rpc + @rpc ||= Bitcoin::Rpc.instance + end +end diff --git a/app/jobs/revert_block_job.rb b/app/jobs/revert_block_job.rb index e21f3af1e..e1e49c928 100644 --- a/app/jobs/revert_block_job.rb +++ b/app/jobs/revert_block_job.rb @@ -111,11 +111,7 @@ def recalculate_udt_accounts(udt_type_hashes, local_tip_block) when "xudt", "omiga_inscription", "xudt_compatible" amount = address.cell_outputs.live.where(cell_type: udt_account.udt_type).where(type_hash:).sum(:udt_amount) udt_account.update!(amount:) - when "m_nft_token" - udt_account.destroy - when "nrc_721_token" - udt_account.destroy - when "spore_cell" + when "m_nft_token", "nrc_721_token", "spore_cell", "did_cell" udt_account.destroy end end diff --git a/app/models/cell_input.rb b/app/models/cell_input.rb index 24fe917b4..cbc35c668 100644 --- a/app/models/cell_input.rb +++ b/app/models/cell_input.rb @@ -13,7 +13,7 @@ class CellInput < ApplicationRecord normal: 0, nervos_dao_deposit: 1, nervos_dao_withdrawing: 2, udt: 3, m_nft_issuer: 4, m_nft_class: 5, m_nft_token: 6, nrc_721_token: 7, nrc_721_factory: 8, cota_registry: 9, cota_regular: 10, spore_cluster: 11, spore_cell: 12, omiga_inscription_info: 13, omiga_inscription: 14, - xudt: 15, unique_cell: 16, xudt_compatible: 17 + xudt: 15, unique_cell: 16, xudt_compatible: 17, did_cell: 18 } def output diff --git a/app/models/cell_output.rb b/app/models/cell_output.rb index 7654c185f..faded8e64 100644 --- a/app/models/cell_output.rb +++ b/app/models/cell_output.rb @@ -29,6 +29,7 @@ class CellOutput < ApplicationRecord xudt: 15, unique_cell: 16, xudt_compatible: 17, + did_cell: 18, } belongs_to :ckb_transaction diff --git a/app/models/ckb_sync/api.rb b/app/models/ckb_sync/api.rb index 9dd401b18..1cf7674dd 100644 --- a/app/models/ckb_sync/api.rb +++ b/app/models/ckb_sync/api.rb @@ -87,6 +87,10 @@ def spore_cell_code_hashes end end + def did_cell_code_hash + Settings.did_cell_code_hash + end + def omiga_inscription_info_code_hash Settings.omiga_inscription_info_code_hash end diff --git a/app/models/ckb_sync/new_node_data_processor.rb b/app/models/ckb_sync/new_node_data_processor.rb index f0368aa5b..84751cf2a 100644 --- a/app/models/ckb_sync/new_node_data_processor.rb +++ b/app/models/ckb_sync/new_node_data_processor.rb @@ -419,7 +419,7 @@ def update_or_create_udt_accounts!(local_block) local_block.cell_outputs.select(:id, :address_id, :type_hash, :cell_type, :type_script_id).each do |udt_output| next unless udt_output.cell_type.in?(%w(udt m_nft_token nrc_721_token - spore_cell omiga_inscription xudt xudt_compatible)) + spore_cell did_cell omiga_inscription xudt xudt_compatible)) address = Address.find(udt_output.address_id) udt_type = udt_type(udt_output.cell_type) @@ -430,7 +430,7 @@ def update_or_create_udt_accounts!(local_block) case udt_type when "nrc_721_token" CkbUtils.parse_nrc_721_args(udt_output.type_script.args).token_id - when "spore_cell" + when "spore_cell", "did_cell" udt_output.type_script.args.hex end udt = Udt.where(type_hash: udt_output.type_hash, udt_type:).select(:id, :udt_type, :full_name, @@ -450,7 +450,7 @@ def update_or_create_udt_accounts!(local_block) CellOutput.where(consumed_by_id: tx_id).select(:id, :address_id, :type_hash, :cell_type).each do |udt_output| next unless udt_output.cell_type.in?(%w(udt m_nft_token nrc_721_token - spore_cell omiga_inscription xudt xudt_compatible)) + spore_cell did_cell omiga_inscription xudt xudt_compatible)) address = Address.find(udt_output.address_id) udt_type = udt_type(udt_output.cell_type) @@ -464,12 +464,8 @@ def update_or_create_udt_accounts!(local_block) when "sudt", "omiga_inscription", "xudt", "xudt_compatible" udt_accounts_attributes << { id: udt_account.id, amount:, created_at: udt.created_at } - when "m_nft_token" - udt_account.destroy unless address.cell_outputs.live.m_nft_token.where(type_hash: udt_output.type_hash).exists? - when "nrc_721_token" - udt_account.destroy unless address.cell_outputs.live.nrc_721_token.where(type_hash: udt_output.type_hash).exists? - when "spore_cell" - udt_account.destroy unless address.cell_outputs.live.spore_cell.where(type_hash: udt_output.type_hash).exists? + when "m_nft_token", "nrc_721_token", "spore_cell", "did_cell" + udt_account.destroy unless address.cell_outputs.live.where(cell_type: udt_type).where(type_hash: udt_output.type_hash).exists? end end end @@ -496,7 +492,7 @@ def udt_account_amount(udt_type, type_hash, address) case udt_type when "sudt" address.cell_outputs.live.udt.where(type_hash:).sum(:udt_amount) - when "xudt", "xudt_compatible", "omiga_inscription", "m_nft_token", "spore_cell" + when "xudt", "xudt_compatible", "omiga_inscription", "m_nft_token", "spore_cell", "did_cell" address.cell_outputs.live.where(cell_type: udt_type).where(type_hash:).sum(:udt_amount) else 0 @@ -591,7 +587,7 @@ def build_udts!(local_block, outputs, outputs_data) outputs.each do |tx_index, items| items.each_with_index do |output, index| cell_type = cell_type(output.type, outputs_data[tx_index][index]) - next unless cell_type.in?(%w(udt m_nft_token nrc_721_token spore_cell + next unless cell_type.in?(%w(udt m_nft_token nrc_721_token spore_cell did_cell omiga_inscription_info omiga_inscription xudt xudt_compatible)) type_hash, parsed_udt_type = @@ -643,17 +639,17 @@ def build_udts!(local_block, outputs, outputs_data) nft_token_attr[:icon_file] = parsed_class_data.renderer nft_token_attr[:published] = true end - when "spore_cell" + when "spore_cell", "did_cell" nft_token_attr[:published] = true parsed_spore_cell = CkbUtils.parse_spore_cell_data(outputs_data[tx_index][index]) if parsed_spore_cell[:cluster_id].present? binary_hashes = CkbUtils.hexes_to_bins_sql(CkbSync::Api.instance.spore_cluster_code_hashes) - spore_cluster_type_ids = TypeScript.where("code_hash IN (#{binary_hashes})").where(hash_type: "data1", - args: parsed_spore_cell[:cluster_id]).pluck(:id) - - spore_cluster_cell = CellOutput.live.where(type_script_id: spore_cluster_type_ids).last - parsed_cluster_data = CkbUtils.parse_spore_cluster_data(spore_cluster_cell.data) - nft_token_attr[:full_name] = parsed_cluster_data[:name] + spore_cluster_type_ids = TypeScript.where("code_hash IN (#{binary_hashes})").where(hash_type: "data1", args: parsed_spore_cell[:cluster_id]).pluck(:id) + if spore_cluster_type_ids.present? + spore_cluster_cell = CellOutput.live.where(type_script_id: spore_cluster_type_ids).last + parsed_cluster_data = CkbUtils.parse_spore_cluster_data(spore_cluster_cell.data) + nft_token_attr[:full_name] = parsed_cluster_data[:name] + end end when "nrc_721_token" factory_cell = CkbUtils.parse_nrc_721_args(output.type.args) @@ -1009,7 +1005,7 @@ def build_cell_inputs( dao_address_ids[tx_index] << address_id change_rec[:dao_txs] ||= Set.new change_rec[:dao_txs] << ckb_txs[tx_index]["tx_hash"] - elsif cell_type.in?(%w(m_nft_token nrc_721_token spore_cell)) + elsif cell_type.in?(%w(m_nft_token nrc_721_token spore_cell did_cell)) token_transfer_ckb_tx_ids << ckb_txs[tx_index]["id"] end @@ -1114,7 +1110,7 @@ def build_cell_outputs!( contained_udt_ids[tx_index] << Udt.where( type_hash: item.type.compute_hash, udt_type: "xudt_compatible", ).pick(:id) - elsif attr[:cell_type].in?(%w(m_nft_token nrc_721_token spore_cell)) + elsif attr[:cell_type].in?(%w(m_nft_token nrc_721_token spore_cell did_cell)) token_transfer_ckb_tx_ids << ckb_txs[tx_index]["id"] end diff --git a/app/models/suggest_query.rb b/app/models/suggest_query.rb index 96cea6f1d..7f3e90a9f 100644 --- a/app/models/suggest_query.rb +++ b/app/models/suggest_query.rb @@ -100,7 +100,7 @@ def find_udt_by_type_hash udt = Udt.find_by(type_hash: query_key, published: true) return unless udt.present? - if udt.spore_cell? + if udt.spore_cell? || udt.did_cell? type_script = TypeScript.find_by(script_hash: query_key) return unless type_script diff --git a/app/models/udt.rb b/app/models/udt.rb index 991015901..cb8caae50 100644 --- a/app/models/udt.rb +++ b/app/models/udt.rb @@ -5,9 +5,10 @@ class Udt < ApplicationRecord has_one :udt_verification has_one :omiga_inscription_info has_one :xudt_tag + has_many :udt_holder_allocations enum udt_type: { sudt: 0, m_nft_token: 1, nrc_721_token: 2, spore_cell: 3, - omiga_inscription: 4, xudt: 5, xudt_compatible: 6 } + omiga_inscription: 4, xudt: 5, xudt_compatible: 6, did_cell: 7 } validates_presence_of :total_amount validates :decimal, diff --git a/app/models/udt_account.rb b/app/models/udt_account.rb index 4b14f927e..f4aa9e2c2 100644 --- a/app/models/udt_account.rb +++ b/app/models/udt_account.rb @@ -1,6 +1,6 @@ class UdtAccount < ApplicationRecord enum udt_type: { sudt: 0, m_nft_token: 1, nrc_721_token: 2, spore_cell: 3, - omiga_inscription: 4, xudt: 5, xudt_compatible: 6 } + omiga_inscription: 4, xudt: 5, xudt_compatible: 6, did_cell: 7 } belongs_to :address belongs_to :udt diff --git a/app/models/udt_holder_allocation.rb b/app/models/udt_holder_allocation.rb new file mode 100644 index 000000000..79b7bce1c --- /dev/null +++ b/app/models/udt_holder_allocation.rb @@ -0,0 +1,21 @@ +class UdtHolderAllocation < ApplicationRecord + belongs_to :udt + belongs_to :contract, optional: true +end + +# == Schema Information +# +# Table name: udt_holder_allocations +# +# id :bigint not null, primary key +# udt_id :bigint not null +# contract_id :bigint +# ckb_holder_count :integer default(0), not null +# btc_holder_count :integer default(0), not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_udt_holder_allocations_on_udt_id (udt_id) +# diff --git a/app/serializers/address_serializer.rb b/app/serializers/address_serializer.rb index 5ae410fe1..d02767fc3 100644 --- a/app/serializers/address_serializer.rb +++ b/app/serializers/address_serializer.rb @@ -111,7 +111,7 @@ class AddressSerializer udt_type: udt_account.udt_type, udt_type_script: udt&.type_script, } - elsif udt_account.udt_type == "spore_cell" + elsif udt_account.udt_type.in?(["spore_cell", "did_cell"]) ts = TypeScript.where(script_hash: udt_account.type_hash).first if ts data = ts.cell_outputs.order("id desc").first.data diff --git a/app/services/portfolios/udt_accounts_statistic.rb b/app/services/portfolios/udt_accounts_statistic.rb index 7aa815431..081cde63c 100644 --- a/app/services/portfolios/udt_accounts_statistic.rb +++ b/app/services/portfolios/udt_accounts_statistic.rb @@ -62,7 +62,7 @@ def nft_accounts udt_icon_file: "#{udt_account.udt.nrc_factory_cell&.base_token_uri}/#{udt_account.nft_token_id}", udt_type: udt_account.udt_type, } - when "spore_cell" + when "spore_cell", "did_cell" ts = TypeScript.where(script_hash: udt_account.type_hash).first if ts data = ts.cell_outputs.order(id: :desc).first.data diff --git a/app/utils/ckb_utils.rb b/app/utils/ckb_utils.rb index 9ea314854..fe547c4af 100644 --- a/app/utils/ckb_utils.rb +++ b/app/utils/ckb_utils.rb @@ -407,7 +407,7 @@ def self.cell_type(type_script, output_data) CkbSync::Api.instance.issuer_script_code_hash, CkbSync::Api.instance.token_class_script_code_hash, CkbSync::Api.instance.token_script_code_hash, CkbSync::Api.instance.cota_registry_code_hash, CkbSync::Api.instance.cota_regular_code_hash, CkbSync::Api.instance.omiga_inscription_info_code_hash, - CkbSync::Api.instance.xudt_code_hash, CkbSync::Api.instance.unique_cell_code_hash, CkbSync::Api.instance.xudt_compatible_code_hash + CkbSync::Api.instance.xudt_code_hash, CkbSync::Api.instance.unique_cell_code_hash, CkbSync::Api.instance.xudt_compatible_code_hash, CkbSync::Api.instance.did_cell_code_hash ].include?(type_script&.code_hash) && type_script&.hash_type == "type") || is_nrc_721_token_cell?(output_data) || is_nrc_721_factory_cell?(output_data) || @@ -445,6 +445,8 @@ def self.cell_type(type_script, output_data) "spore_cluster" when *CkbSync::Api.instance.spore_cell_code_hashes "spore_cell" + when CkbSync::Api.instance.did_cell_code_hash + "did_cell" when CkbSync::Api.instance.omiga_inscription_info_code_hash "omiga_inscription_info" when CkbSync::Api.instance.xudt_compatible_code_hash diff --git a/app/workers/bitcoin_transaction_detect_worker.rb b/app/workers/bitcoin_transaction_detect_worker.rb index fd48d64c8..9526555a5 100644 --- a/app/workers/bitcoin_transaction_detect_worker.rb +++ b/app/workers/bitcoin_transaction_detect_worker.rb @@ -2,7 +2,7 @@ class BitcoinTransactionDetectWorker include Sidekiq::Worker sidekiq_options queue: "bitcoin" - BITCOIN_RPC_BATCH_SIZE = 30 + BITCOIN_RPC_BATCH_SIZE = 200 attr_accessor :block, :txids, :rgbpp_cell_ids, :btc_time_cell_ids @@ -24,9 +24,13 @@ def perform(number) # batch fetch bitcoin raw transactions cache_raw_transactions! # import rgbpp cells - @rgbpp_cell_ids.each { ImportRgbppCellJob.perform_now(_1) } + @rgbpp_cell_ids.each_slice(BITCOIN_RPC_BATCH_SIZE) do + ImportRgbppCellsJob.perform_now(_1) + end # import btc time cells - @btc_time_cell_ids.each { ImportBtcTimeCellJob.perform_now(_1) } + @btc_time_cell_ids.each_slice(BITCOIN_RPC_BATCH_SIZE) do + ImportBtcTimeCellsJob.perform_now(_1) + end # update bitcoin annotation build_bitcoin_annotations! end @@ -79,7 +83,7 @@ def cache_raw_transactions! end end - Rails.cache.write_multi(to_cache, expires_in: 10.minutes) if to_cache.present? + Rails.cache.write_multi(to_cache, expires_in: 30.minutes) if to_cache.present? rescue StandardError => e Rails.logger.error "cache raw transactions(#{@txids.uniq}) failed: #{e.message}" end diff --git a/app/workers/generate_Udt_holders_worker.rb b/app/workers/generate_Udt_holders_worker.rb deleted file mode 100644 index ee51aa6c3..000000000 --- a/app/workers/generate_Udt_holders_worker.rb +++ /dev/null @@ -1,23 +0,0 @@ -class GenerateUdtHoldersWorker - include Sidekiq::Worker - sidekiq_options retry: 3 - - def perform(type_hash) - udt = Udt.find_by(type_hash:) - return unless udt - - type_script = TypeScript.find_by(udt.type_script) - return unless type_script - - ckb_address_ids = CellOutput.where(type_script_id: type_script.id).pluck(:address_id).uniq - bitcoin_address_ids = [] - ckb_address_ids.each_slice(1000) do |address_ids| - ids = BitcoinAddressMapping.where(ckb_address_id: address_ids).pluck(:bitcoin_address_id) - bitcoin_address_ids.concat(ids).uniq! - end - - cache_key = "udt_holders/#{type_hash}" - cache_value = { ckb_holders_count: ckb_address_ids.count, btc_holders_count: bitcoin_address_ids.count } - Rails.cache.write(cache_key, cache_value) - end -end diff --git a/app/workers/generate_udt_holder_allocation_worker.rb b/app/workers/generate_udt_holder_allocation_worker.rb new file mode 100644 index 000000000..133e6fcfa --- /dev/null +++ b/app/workers/generate_udt_holder_allocation_worker.rb @@ -0,0 +1,61 @@ +class GenerateUdtHolderAllocationWorker + include Sidekiq::Worker + sidekiq_options retry: 3 + + def perform(type_hash) + udt = Udt.find_by(type_hash:) + return unless udt + + update_udt_holder_allocation(udt) + update_contract_holder_allocation(udt) + end + + def update_udt_holder_allocation(udt) + type_script = TypeScript.find_by(udt.type_script) + return unless type_script + + holder_allocation = UdtHolderAllocation.find_or_initialize_by(udt:, contract_id: nil) + ckb_address_ids = CellOutput.live.where(type_script:).distinct.pluck(:address_id) + btc_address_ids = [] + ckb_address_ids.each_slice(1000) do |address_ids| + ids = BitcoinAddressMapping.where(ckb_address_id: address_ids).pluck(:bitcoin_address_id) + btc_address_ids.concat(ids).uniq! + end + + holder_allocation.update!(btc_holder_count: btc_address_ids.count) + end + + def update_contract_holder_allocation(udt) + type_script = TypeScript.find_by(udt.type_script) + return unless type_script + + unique_ckb_address_ids = [] + CellOutput.live.where(type_script:).find_in_batches(batch_size: 1000) do |batch| + batch_ckb_address_ids = batch.pluck(:address_id) + excluded_ckb_address_ids = BitcoinAddressMapping.where(ckb_address_id: batch_ckb_address_ids).pluck(:ckb_address_id) + filtered_ckb_address_ids = batch_ckb_address_ids - excluded_ckb_address_ids + unique_ckb_address_ids.concat(filtered_ckb_address_ids).uniq! + end + + allocation_data = {} + unique_ckb_address_ids.each_slice(1000) do |batch_address_ids| + holder_count = CellOutput.joins(:lock_script). + where(address_id: batch_address_ids). + group("lock_scripts.code_hash"). + count("DISTINCT cell_outputs.address_id") + + holder_count.each do |code_hash, count| + allocation_data[code_hash] ||= 0 + allocation_data[code_hash] += count + end + end + + allocation_data.each do |code_hash, count| + contract = Contract.find_by(code_hash:, role: ["LockScript", "lock_script"]) + next unless contract + + allocation = UdtHolderAllocation.find_or_initialize_by(udt:, contract:) + allocation.update!(ckb_holder_count: count) + end + end +end diff --git a/app/workers/token_transfer_detect_worker.rb b/app/workers/token_transfer_detect_worker.rb index ff9fcd484..be18d6ecc 100644 --- a/app/workers/token_transfer_detect_worker.rb +++ b/app/workers/token_transfer_detect_worker.rb @@ -11,7 +11,7 @@ def perform(tx_id) source_collections = [] tx.cell_inputs.each do |input| - if input.cell_type.in?(%w(m_nft_token nrc_721_token spore_cell)) + if input.cell_type.in?(%w(m_nft_token nrc_721_token spore_cell did_cell)) cell = input.previous_cell_output type_script = input.type_script @@ -20,7 +20,7 @@ def perform(tx_id) end tx.cell_outputs.each do |output| - if output.cell_type.in?(%w(m_nft_token nrc_721_token spore_cell)) + if output.cell_type.in?(%w(m_nft_token nrc_721_token spore_cell did_cell)) type_script = output.type_script item = find_or_create_item(output, type_script) attrs = { @@ -76,7 +76,7 @@ def find_or_create_item(cell, type_script) type_script.args[50..-1].hex when "nrc_721_token" type_script.args[132..-1].hex - when "spore_cell" + when "spore_cell", "did_cell" type_script.args.hex end item.save! @@ -89,7 +89,7 @@ def find_or_create_collection(cell, type_script) find_or_create_nrc_721_collection(cell, type_script) when "m_nft_token" find_or_create_m_nft_collection(cell, type_script) - when "spore_cell" + when "spore_cell", "did_cell" find_or_create_spore_collection(cell, type_script) end end diff --git a/app/workers/update_udt_info_worker.rb b/app/workers/update_udt_info_worker.rb index 06f137407..b68946863 100644 --- a/app/workers/update_udt_info_worker.rb +++ b/app/workers/update_udt_info_worker.rb @@ -44,7 +44,7 @@ def perform(block_number) end, unique_by: :type_hash ) - type_hashes.each { GenerateUdtHoldersWorker.perform_async(_1) } + type_hashes.each { GenerateUdtHolderAllocationWorker.perform_async(_1) } end end end diff --git a/config/routes.rb b/config/routes.rb index a4fd5a485..152d7fa07 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -54,6 +54,7 @@ resources :market_data, only: %i[index show] resources :udts, only: %i(index show update) do get :download_csv, on: :collection + get :holder_allocation, on: :member end resources :xudts, only: %i(index show) do get :download_csv, on: :collection diff --git a/config/settings.mainnet.yml b/config/settings.mainnet.yml index b0e1bb287..52583dd77 100644 --- a/config/settings.mainnet.yml +++ b/config/settings.mainnet.yml @@ -24,6 +24,9 @@ nrc_721_token_output_data_header: "0x0ddeff3e8ee03cbf6a2c6920d05c381e" spore_cluster1_code_hash: "0x7366a61534fa7c7e6225ecc0d828ea3b5366adec2b58206f2ee84995fe030075" spore_cell1_code_hash: "0x4a4dce1df3dffff7f8b2cd7dff7303df3b6150c9788cb75dcf6747247132b9f5" +# did cell +did_cell_code_hash: "0xcfba73b58b6f30e70caed8a999748781b164ef9a1e218424a6fb55ebf641cb33" + # omiga inscription info omiga_inscription_info_code_hash: "0x5c33fc69bd72e895a63176147c6ab0bb5758d1c7a32e0914f99f9ec1bed90d41" omiga_inscription_code_hash: "0x7490970e6af9b9fe63fc19fc523a12b2ec69027e6ae484edffb97334f74e8c97" diff --git a/config/settings.testnet.yml b/config/settings.testnet.yml index 470d138f9..8cfbd8363 100644 --- a/config/settings.testnet.yml +++ b/config/settings.testnet.yml @@ -28,6 +28,9 @@ spore_cell1_code_hash: "0x5e063b4c0e7abeaa6a428df3b693521a3050934cf3b0ae97a800d1 spore_cell2_code_hash: "0xbbad126377d45f90a8ee120da988a2d7332c78ba8fd679aab478a19d6c133494" # tag: v1 latest spore_cell3_code_hash: "0x685a60219309029d01310311dba953d67029170ca4848a4ff638e57002130a0d" # tag: 0.2.2-beta.2 +#did cell +did_cell_code_hash: "0x0b1f412fbae26853ff7d082d422c2bdd9e2ff94ee8aaec11240a5b34cc6e890f" + # omiga inscription info omiga_inscription_info_code_hash: "0x50fdea2d0030a8d0b3d69f883b471cab2a29cae6f01923f19cecac0f27fdaaa6" omiga_inscription_code_hash: "0x3a241ceceede72a5f55c8fb985652690f09a517d6c9070f0df0d3572fa03fb70" @@ -89,4 +92,3 @@ rgbpp_code_hash: btc_time_code_hash: - "0x00cdf8fab0f8ac638758ebf5ea5e4052b1d71e8a77b9f43139718621f6849326" - "0x80a09eca26d77cea1f5a69471c59481be7404febf40ee90f886c36a948385b55" - diff --git a/db/migrate/20240625032839_create_udt_holder_allocations.rb b/db/migrate/20240625032839_create_udt_holder_allocations.rb new file mode 100644 index 000000000..b5107e1dc --- /dev/null +++ b/db/migrate/20240625032839_create_udt_holder_allocations.rb @@ -0,0 +1,12 @@ +class CreateUdtHolderAllocations < ActiveRecord::Migration[7.0] + def change + create_table :udt_holder_allocations do |t| + t.bigint :udt_id, null: false, index: true + t.bigint :contract_id + t.integer :ckb_holder_count, null: false, default: 0 + t.integer :btc_holder_count, null: false, default: 0 + + t.timestamps + end + end +end diff --git a/db/structure.sql b/db/structure.sql index f3527c8df..a2d1b67c7 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -2400,6 +2400,40 @@ CREATE SEQUENCE public.udt_accounts_id_seq ALTER SEQUENCE public.udt_accounts_id_seq OWNED BY public.udt_accounts.id; +-- +-- Name: udt_holder_allocations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.udt_holder_allocations ( + id bigint NOT NULL, + udt_id bigint NOT NULL, + contract_id bigint, + ckb_holder_count integer DEFAULT 0 NOT NULL, + btc_holder_count integer DEFAULT 0 NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: udt_holder_allocations_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.udt_holder_allocations_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: udt_holder_allocations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.udt_holder_allocations_id_seq OWNED BY public.udt_holder_allocations.id; + + -- -- Name: udt_transactions; Type: TABLE; Schema: public; Owner: - -- @@ -3010,6 +3044,13 @@ ALTER TABLE ONLY public.type_scripts ALTER COLUMN id SET DEFAULT nextval('public ALTER TABLE ONLY public.udt_accounts ALTER COLUMN id SET DEFAULT nextval('public.udt_accounts_id_seq'::regclass); +-- +-- Name: udt_holder_allocations id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.udt_holder_allocations ALTER COLUMN id SET DEFAULT nextval('public.udt_holder_allocations_id_seq'::regclass); + + -- -- Name: udt_verifications id; Type: DEFAULT; Schema: public; Owner: - -- @@ -3532,6 +3573,14 @@ ALTER TABLE ONLY public.udt_accounts ADD CONSTRAINT udt_accounts_pkey PRIMARY KEY (id); +-- +-- Name: udt_holder_allocations udt_holder_allocations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.udt_holder_allocations + ADD CONSTRAINT udt_holder_allocations_pkey PRIMARY KEY (id); + + -- -- Name: udt_verifications udt_verifications_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -4563,6 +4612,13 @@ CREATE UNIQUE INDEX index_udt_accounts_on_type_hash_and_address_id ON public.udt CREATE INDEX index_udt_accounts_on_udt_id ON public.udt_accounts USING btree (udt_id); +-- +-- Name: index_udt_holder_allocations_on_udt_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_udt_holder_allocations_on_udt_id ON public.udt_holder_allocations USING btree (udt_id); + + -- -- Name: index_udt_transactions_on_ckb_transaction_id; Type: INDEX; Schema: public; Owner: - -- @@ -5181,6 +5237,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240507041552'), ('20240509074313'), ('20240513055849'), -('20240620083123'); +('20240620083123'), +('20240625032839'); diff --git a/test/controllers/api/v2/nft/collections_controller_test.rb b/test/controllers/api/v2/nft/collections_controller_test.rb index 53e9701c2..4b36d49e6 100644 --- a/test/controllers/api/v2/nft/collections_controller_test.rb +++ b/test/controllers/api/v2/nft/collections_controller_test.rb @@ -13,6 +13,24 @@ class NFT::CollectionsControllerTest < ActionDispatch::IntegrationTest assert_equal ["invalid"], JSON.parse(response.body)["data"].last["tags"] end + test "should filter by tags" do + create :token_collection, name: "token1", tags: ["layer-1-asset", "rgbpp-compatible"] + create :token_collection, name: "token2", tags: ["layer-1-asset", "rgbpp-compatible"] + + get api_v2_nft_collections_url, params: { tags: "layer-1-asset,rgbpp-compatible" } + assert_response :success + assert_equal JSON.parse(response.body)["data"].size, 2 + end + + test "should filter by tags but not match" do + create :token_collection, name: "token1", tags: ["layer-1-asset", "rgbpp-compatible"] + create :token_collection, name: "token2", tags: ["layer-1-asset", "rgbpp-compatible"] + + get api_v2_nft_collections_url, params: { tags: "layer-1-asset,invalid" } + assert_response :success + assert_equal JSON.parse(response.body)["data"].size, 0 + end + test "sort by block_timestamp asc" do block1 = create(:block, :with_block_hash, timestamp: 10.days.ago.to_i * 1000) block3 = create(:block, :with_block_hash, timestamp: 1.day.ago.to_i * 1000) diff --git a/test/factories/udt_holder_allocations.rb b/test/factories/udt_holder_allocations.rb new file mode 100644 index 000000000..5c46b7a17 --- /dev/null +++ b/test/factories/udt_holder_allocations.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :udt_holder_allocation do + + end +end diff --git a/test/models/udt_holder_allocation_test.rb b/test/models/udt_holder_allocation_test.rb new file mode 100644 index 000000000..4f15bcb15 --- /dev/null +++ b/test/models/udt_holder_allocation_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class UdtHolderAllocationTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end