From 15df613cc4fa3f2fc4455854313d8587f5ca11e4 Mon Sep 17 00:00:00 2001 From: Miles Zhang Date: Tue, 30 Jan 2024 09:44:26 +0800 Subject: [PATCH] feat: address live cells contoller (#1610) Signed-off-by: Miles Zhang --- .../api/v1/address_live_cells_controller.rb | 38 ++++++++ app/models/cell_output.rb | 35 +++++++- app/models/concerns/display_cells.rb | 30 ++++--- app/serializers/cell_output_serializer.rb | 42 +++++++++ config/routes.rb | 1 + ...ddress_dao_transactions_controller_test.rb | 23 ++--- .../v1/address_live_cells_controller_test.rb | 89 +++++++++++++++++++ test/factories/cell_output.rb | 17 +++- 8 files changed, 249 insertions(+), 26 deletions(-) create mode 100644 app/controllers/api/v1/address_live_cells_controller.rb create mode 100644 app/serializers/cell_output_serializer.rb create mode 100644 test/controllers/api/v1/address_live_cells_controller_test.rb diff --git a/app/controllers/api/v1/address_live_cells_controller.rb b/app/controllers/api/v1/address_live_cells_controller.rb new file mode 100644 index 000000000..3e379309d --- /dev/null +++ b/app/controllers/api/v1/address_live_cells_controller.rb @@ -0,0 +1,38 @@ +module Api + module V1 + class AddressLiveCellsController < ApplicationController + before_action :validate_pagination_params, :pagination_params + + def show + address = Address.find_address!(params[:id]) + raise Api::V1::Exceptions::AddressNotFoundError if address.is_a?(NullAddress) + + order_by, asc_or_desc = live_cells_ordering + @addresses = address.cell_outputs.live.order(order_by => asc_or_desc).page(@page).per(@page_size).fast_page + options = FastJsonapi::PaginationMetaGenerator.new( + request:, + records: @addresses, + page: @page, + page_size: @page_size, + ).call + render json: CellOutputSerializer.new(@addresses, options).serialized_json + end + + private + + def pagination_params + @page = params[:page] || 1 + @page_size = params[:page_size] || CellOutput.default_per_page + end + + def live_cells_ordering + sort, order = params.fetch(:sort, "block_timestamp.desc").split(".", 2) + if order.nil? || !order.match?(/^(asc|desc)$/i) + order = "asc" + end + + [sort, order] + end + end + end +end diff --git a/app/models/cell_output.rb b/app/models/cell_output.rb index 72950988c..279825314 100644 --- a/app/models/cell_output.rb +++ b/app/models/cell_output.rb @@ -2,6 +2,11 @@ class CellOutput < ApplicationRecord SYSTEM_TX_HASH = "0x0000000000000000000000000000000000000000000000000000000000000000".freeze MAXIMUM_DOWNLOADABLE_SIZE = 64000 MIN_SUDT_AMOUNT_BYTESIZE = 16 + MAX_PAGINATES_PER = 100 + DEFAULT_PAGINATES_PER = 10 + paginates_per DEFAULT_PAGINATES_PER + max_paginates_per MAX_PAGINATES_PER + enum status: { live: 0, dead: 1, pending: 2, rejected: 3 } enum cell_type: { normal: 0, @@ -19,7 +24,7 @@ class CellOutput < ApplicationRecord spore_cell: 12, omiga_inscription_info: 13, omiga_inscription: 14, - xudt: 15 + xudt: 15, } belongs_to :ckb_transaction @@ -267,6 +272,34 @@ def nrc_721_nft_info CkbUtils.hash_value_to_s(value) end + def omiga_inscription_info + return unless cell_type.in?(%w(omiga_inscription_info omiga_inscription)) + + case cell_type + when "omiga_inscription_info" + info = OmigaInscriptionInfo.find_by(code_hash: type_script.code_hash, + hash_type: type_script.hash_type, + args: type_script.args) + value = { + symbol: info.symbol, + name: info.name, + decimal: info.decimal, + amount: 0, + } + when "omiga_inscription" + udt = Udt.find_by(type_hash:) + value = { + symbol: udt.symbol, + name: udt.name, + decimal: udt.decimal, + amount: udt_amount, + } + else + raise "invalid cell type" + end + CkbUtils.hash_value_to_s(value) + end + def create_token case cell_type when "m_nft_class" diff --git a/app/models/concerns/display_cells.rb b/app/models/concerns/display_cells.rb index 1b95a91d5..b7caafb9a 100644 --- a/app/models/concerns/display_cells.rb +++ b/app/models/concerns/display_cells.rb @@ -34,8 +34,8 @@ def cellbase_display_inputs occupied_capacity: nil, address_hash: nil, target_block_number: cellbase.target_block_number, - generated_tx_hash: tx_hash - ) + generated_tx_hash: tx_hash, + ), ] end @@ -55,7 +55,7 @@ def cellbase_display_outputs proposal_reward: cellbase.proposal_reward, secondary_reward: cellbase.secondary_reward, status: output.status, - consumed_tx_hash: consumed_tx_hash + consumed_tx_hash:, ) end end @@ -73,8 +73,8 @@ def normal_tx_display_inputs(cell_inputs_for_display) cell_index: cell_input.previous_index, since: { raw: hex_since(cell_input.since.to_i), - median_timestamp: cell_input.block&.median_timestamp.to_i - } + median_timestamp: cell_input.block&.median_timestamp.to_i, + }, }) end @@ -89,8 +89,8 @@ def normal_tx_display_inputs(cell_inputs_for_display) cell_type: previous_cell_output.cell_type, since: { raw: hex_since(cell_input.since.to_i), - median_timestamp: cell_input.block&.median_timestamp.to_i - } + median_timestamp: cell_input.block&.median_timestamp.to_i, + }, } if previous_cell_output.nervos_dao_withdrawing? @@ -122,8 +122,8 @@ def normal_tx_display_outputs(cell_outputs_for_display) occupied_capacity: output.occupied_capacity, address_hash: output.address_hash, status: output.status, - consumed_tx_hash: consumed_tx_hash, - cell_type: output.cell_type + consumed_tx_hash:, + cell_type: output.cell_type, } display_output.merge!(attributes_for_udt_cell(output)) if output.udt? @@ -135,6 +135,9 @@ def normal_tx_display_outputs(cell_outputs_for_display) if output.cell_type.in?(%w(nrc_721_token nrc_721_factory)) display_output.merge!(attributes_for_nrc_721_cell(output)) end + if output.cell_type.in?(%w(omiga_inscription_info omiga_inscription)) + display_output.merge!(attributes_for_omiga_inscription_cell(output)) + end CkbUtils.hash_value_to_s(display_output) end @@ -165,6 +168,11 @@ def attributes_for_nrc_721_cell(nrc_721_cell) { nrc_721_token_info: info, extra_info: info } end + def attributes_for_omiga_inscription_cell(omiga_inscription_cell) + info = omiga_inscription_cell.omiga_inscription_info + { omiga_inscription_info: info, extra_info: info } + end + def attributes_for_dao_input(nervos_dao_withdrawing_cell, is_phase2 = true) nervos_dao_withdrawing_cell_generated_tx = nervos_dao_withdrawing_cell.ckb_transaction nervos_dao_deposit_cell = nervos_dao_withdrawing_cell_generated_tx. @@ -181,7 +189,7 @@ def attributes_for_dao_input(nervos_dao_withdrawing_cell, is_phase2 = true) compensation_started_timestamp: compensation_started_block.timestamp, compensation_ended_block_number: compensation_ended_block.number, compensation_ended_timestamp: compensation_ended_block.timestamp, - interest: interest + interest:, } if is_phase2 @@ -194,7 +202,7 @@ def attributes_for_dao_input(nervos_dao_withdrawing_cell, is_phase2 = true) end def hex_since(int_since_value) - return "0x#{int_since_value.to_s(16).rjust(16, '0')}" + "0x#{int_since_value.to_s(16).rjust(16, '0')}" end end end diff --git a/app/serializers/cell_output_serializer.rb b/app/serializers/cell_output_serializer.rb new file mode 100644 index 000000000..27e22829f --- /dev/null +++ b/app/serializers/cell_output_serializer.rb @@ -0,0 +1,42 @@ +class CellOutputSerializer + include FastJsonapi::ObjectSerializer + + attributes :cell_type, :tx_hash, :cell_index, :type_hash, :data + + attribute :capacity do |object| + object.capacity.to_s + end + + attribute :occupied_capacity do |object| + object.occupied_capacity.to_s + end + + attribute :block_timestamp do |object| + object.block_timestamp.to_s + end + + attribute :type_script do |object| + object&.type_script&.to_node + end + + attribute :lock_script do |object| + object.lock_script.to_node + end + + attribute :extra_info do |object| + case object.cell_type + when "udt" + object.udt_info + when "cota_registry" + object.cota_registry_info + when "cota_regular" + object.cota_regular_info + when "m_nft_issuer", "m_nft_class", "m_nft_token" + object.m_nft_info + when "nrc_721_token", "nrc_721_factory" + object.nrc_721_nft_info + when "omiga_inscription_info", "omiga_inscription" + object.omiga_inscription_info + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 6dfbc23fa..aeadf4698 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -70,6 +70,7 @@ resources :monetary_data, only: :show resources :udt_verifications, only: :update resources :address_pending_transactions, only: :show + resources :address_live_cells, only: :show end end draw "v2" diff --git a/test/controllers/api/v1/address_dao_transactions_controller_test.rb b/test/controllers/api/v1/address_dao_transactions_controller_test.rb index 7e736a078..54551ab1b 100644 --- a/test/controllers/api/v1/address_dao_transactions_controller_test.rb +++ b/test/controllers/api/v1/address_dao_transactions_controller_test.rb @@ -62,7 +62,7 @@ class AddressDaoTransactionsControllerTest < ActionDispatch::IntegrationTest valid_get api_v1_address_dao_transaction_url(address.address_hash) - options = FastJsonapi::PaginationMetaGenerator.new(request: request, records: ckb_dao_transactions, page: page, page_size: page_size).call + options = FastJsonapi::PaginationMetaGenerator.new(request:, records: ckb_dao_transactions, page:, page_size:).call assert_equal CkbTransactionsSerializer.new(ckb_dao_transactions, options.merge(params: { previews: true })).serialized_json, response.body end @@ -76,7 +76,7 @@ class AddressDaoTransactionsControllerTest < ActionDispatch::IntegrationTest valid_get api_v1_address_dao_transaction_url(address.lock_hash) - options = FastJsonapi::PaginationMetaGenerator.new(request: request, records: ckb_dao_transactions, page: page, page_size: page_size).call + options = FastJsonapi::PaginationMetaGenerator.new(request:, records: ckb_dao_transactions, page:, page_size:).call assert_equal CkbTransactionsSerializer.new(ckb_dao_transactions, options.merge(params: { previews: true })).serialized_json, response.body end @@ -89,7 +89,8 @@ class AddressDaoTransactionsControllerTest < ActionDispatch::IntegrationTest response_tx_transaction = json["data"].first - assert_equal %w(block_number block_timestamp display_inputs display_inputs_count display_outputs display_outputs_count income is_cellbase transaction_hash created_at create_timestamp).sort, response_tx_transaction["attributes"].keys.sort + assert_equal %w(block_number block_timestamp display_inputs display_inputs_count display_outputs display_outputs_count income is_cellbase transaction_hash created_at create_timestamp).sort, + response_tx_transaction["attributes"].keys.sort end test "should return error object when no records found by id" do @@ -149,9 +150,9 @@ class AddressDaoTransactionsControllerTest < ActionDispatch::IntegrationTest fake_dao_deposit_transaction(30, address) address_dao_transactions = address.ckb_dao_transactions.recent.page(page).per(page_size) - valid_get api_v1_address_dao_transaction_url(address.address_hash), params: { page: page } + valid_get api_v1_address_dao_transaction_url(address.address_hash), params: { page: } records_counter = RecordCounters::AddressDaoTransactions.new(address) - options = FastJsonapi::PaginationMetaGenerator.new(request: request, records: address_dao_transactions, page: page, page_size: page_size, records_counter: records_counter).call + options = FastJsonapi::PaginationMetaGenerator.new(request:, records: address_dao_transactions, page:, page_size:, records_counter:).call response_transaction = CkbTransactionsSerializer.new(address_dao_transactions, options.merge(params: { previews: true })).serialized_json assert_equal response_transaction, response.body @@ -165,10 +166,10 @@ class AddressDaoTransactionsControllerTest < ActionDispatch::IntegrationTest fake_dao_deposit_transaction(15, address) address_dao_transactions = address.ckb_dao_transactions.recent.page(page).per(page_size) - valid_get api_v1_address_dao_transaction_url(address.address_hash), params: { page_size: page_size } + valid_get api_v1_address_dao_transaction_url(address.address_hash), params: { page_size: } records_counter = RecordCounters::AddressDaoTransactions.new(address) - options = FastJsonapi::PaginationMetaGenerator.new(request: request, records: address_dao_transactions, page: page, page_size: page_size, records_counter: records_counter).call + options = FastJsonapi::PaginationMetaGenerator.new(request:, records: address_dao_transactions, page:, page_size:, records_counter:).call response_transaction = CkbTransactionsSerializer.new(address_dao_transactions, options.merge(params: { previews: true })).serialized_json assert_equal response_transaction, response.body @@ -182,10 +183,10 @@ class AddressDaoTransactionsControllerTest < ActionDispatch::IntegrationTest fake_dao_deposit_transaction(30, address) address_dao_transactions = address.ckb_dao_transactions.recent.page(page).per(page_size) - valid_get api_v1_address_dao_transaction_url(address.address_hash), params: { page: page, page_size: page_size } + valid_get api_v1_address_dao_transaction_url(address.address_hash), params: { page:, page_size: } records_counter = RecordCounters::AddressDaoTransactions.new(address) - options = FastJsonapi::PaginationMetaGenerator.new(request: request, records: address_dao_transactions, page: page, page_size: page_size, records_counter: records_counter).call + options = FastJsonapi::PaginationMetaGenerator.new(request:, records: address_dao_transactions, page:, page_size:, records_counter:).call response_transaction = CkbTransactionsSerializer.new(address_dao_transactions, options.merge(params: { previews: true })).serialized_json assert_equal response_transaction, response.body @@ -198,10 +199,10 @@ class AddressDaoTransactionsControllerTest < ActionDispatch::IntegrationTest fake_dao_deposit_transaction(3, address) address_dao_transactions = address.ckb_dao_transactions.recent.page(page).per(page_size) - valid_get api_v1_address_dao_transaction_url(address.address_hash), params: { page: page, page_size: page_size } + valid_get api_v1_address_dao_transaction_url(address.address_hash), params: { page:, page_size: } records_counter = RecordCounters::AddressDaoTransactions.new(address) - options = FastJsonapi::PaginationMetaGenerator.new(request: request, records: address_dao_transactions, page: page, page_size: page_size, records_counter: records_counter).call + options = FastJsonapi::PaginationMetaGenerator.new(request:, records: address_dao_transactions, page:, page_size:, records_counter:).call response_transaction = CkbTransactionsSerializer.new(address_dao_transactions, options.merge(params: { previews: true })).serialized_json assert_equal [], json["data"] diff --git a/test/controllers/api/v1/address_live_cells_controller_test.rb b/test/controllers/api/v1/address_live_cells_controller_test.rb new file mode 100644 index 000000000..e17842b6e --- /dev/null +++ b/test/controllers/api/v1/address_live_cells_controller_test.rb @@ -0,0 +1,89 @@ +require "test_helper" + +module Api + module V1 + class AddressLiveCellsControllerTest < ActionDispatch::IntegrationTest + test "should get success code when call show" do + address = create(:address, :with_transactions) + + valid_get api_v1_address_live_cell_url(address.address_hash) + + assert_response :success + end + + test "should set right content type when call show" do + address = create(:address, :with_transactions) + + valid_get api_v1_address_live_cell_url(address.address_hash) + + assert_equal "application/vnd.api+json", response.media_type + end + + test "should return no live cell" do + address = create(:address, :with_udt_transactions) + valid_get api_v1_address_live_cell_url(address.address_hash) + + assert_equal ({ "data" => [], "meta" => { "total" => 0, "page_size" => 20 } }), json + end + + test "should return all live cells" do + address = create(:address) + block = create(:block, :with_block_hash) + transaction = create(:ckb_transaction, block:) + udt = create(:udt, :omiga_inscription, full_name: "CKB Fist Inscription", + symbol: "CKBI", decimal: 8) + info = udt.omiga_inscription_info + address_lock = create(:lock_script, address_id: address.id) + info_ts = create(:type_script, + args: "0xcd89d8f36593a9a82501c024c5cdc4877ca11c5b3d5831b3e78334aecb978f0d", + code_hash: "0x50fdea2d0030a8d0b3d69f883b471cab2a29cae6f01923f19cecac0f27fdaaa6", + hash_type: "type") + create(:cell_output, address:, + block:, + ckb_transaction: transaction, + capacity: 1000000000000, + occupied_capacity: 100000000000, + tx_hash: transaction.tx_hash, + block_timestamp: block.timestamp, + status: "live", + type_hash: info.type_hash, + cell_index: 0, + lock_script_id: address_lock.id, + type_script_id: info_ts.id, + cell_type: "omiga_inscription_info", + data: "0x0814434b42204669737420496e736372697074696f6e04434b4249a69f54bf339dd121febe64cb0be3a2cf366a8b13ec1a5ae4bebdccb9039c7efa0040075af0750700000000000000000000e8764817000000000000000000000002") + valid_get api_v1_address_live_cell_url(address.address_hash) + assert_equal ({ "cell_type" => "omiga_inscription_info", + "tx_hash" => transaction.tx_hash, + "cell_index" => 0, + "type_hash" => info.type_hash, + "data" => "0x0814434b42204669737420496e736372697074696f6e04434b4249a69f54bf339dd121febe64cb0be3a2cf366a8b13ec1a5ae4bebdccb9039c7efa0040075af0750700000000000000000000e8764817000000000000000000000002", + "capacity" => "1000000000000.0", + "occupied_capacity" => "100000000000", + "block_timestamp" => block.timestamp.to_s, + "type_script" => { "args" => "0xcd89d8f36593a9a82501c024c5cdc4877ca11c5b3d5831b3e78334aecb978f0d", "code_hash" => "0x50fdea2d0030a8d0b3d69f883b471cab2a29cae6f01923f19cecac0f27fdaaa6", + "hash_type" => "type" }, + "lock_script" => { "args" => address_lock.args, "code_hash" => address_lock.code_hash, + "hash_type" => "type" }, + "extra_info" => { "symbol" => "CKBI", "name" => "CKB Fist Inscription", "decimal" => "8.0", "amount" => "0" } }), + json["data"].first["attributes"] + end + + test "should paginate and asc sort live cells" do + address = create(:address) + address_lock = create(:lock_script, address_id: address.id) + outputs = create_list(:cell_output, 10, :address_live_cells, lock_script: address_lock, address_id: address.id) + valid_get api_v1_address_live_cell_url(address.address_hash), params: { page: 1, page_size: 5, sort: "block_timestamp.asc" } + assert_equal outputs.first.block_timestamp.to_s, json["data"].first["attributes"]["block_timestamp"] + end + + test "should paginate and desc sort live cells" do + address = create(:address) + address_lock = create(:lock_script, address_id: address.id) + outputs = create_list(:cell_output, 10, :address_live_cells, lock_script: address_lock, address_id: address.id) + valid_get api_v1_address_live_cell_url(address.address_hash), params: { page: 1, page_size: 5, sort: "block_timestamp.desc" } + assert_equal outputs.last.block_timestamp.to_s, json["data"].first["attributes"]["block_timestamp"] + end + end + end +end diff --git a/test/factories/cell_output.rb b/test/factories/cell_output.rb index a8ce7fa0c..299889d18 100644 --- a/test/factories/cell_output.rb +++ b/test/factories/cell_output.rb @@ -8,23 +8,34 @@ data { nil } end cell_type { "normal" } + sequence :block_timestamp do |n| + (Time.now.to_i + n) * 1000 + end lock_script trait :with_full_transaction do before(:create) do |cell_output, _evaluator| ckb_transaction = create(:ckb_transaction, :with_cell_output_and_lock_script, block: cell_output.block) - cell_output.update(ckb_transaction: ckb_transaction) + cell_output.update(ckb_transaction:) lock = create(:lock_script, cell_output_id: cell_output.id, hash_type: "type") type = create(:type_script, cell_output_id: cell_output.id, hash_type: "type") cell_output.update(tx_hash: ckb_transaction.tx_hash, lock_script_id: lock.id, type_script_id: type.id) end end + trait :address_live_cells do + before(:create) do |cell_output, _evaluator| + block = create(:block, :with_block_hash) + ckb_transaction = create(:ckb_transaction, :with_cell_output_and_lock_script) + cell_output.update(ckb_transaction:, block:) + end + end + trait :with_full_transaction_but_no_type_script do before(:create) do |cell_output, _evaluator| block = create(:block, :with_block_hash) ckb_transaction = create(:ckb_transaction, :with_cell_output_and_lock_script) - cell_output.update(ckb_transaction: ckb_transaction, block: block) + cell_output.update(ckb_transaction:, block:) lock = create(:lock_script, cell_output_id: cell_output.id) cell_output.update(lock_script_id: lock.id) end @@ -40,7 +51,7 @@ cell.address.increment! :live_cells_count end AccountBook.upsert({ ckb_transaction_id: cell.ckb_transaction_id, address_id: cell.address_id }, - unique_by: [:address_id, :ckb_transaction_id]) + unique_by: %i[address_id ckb_transaction_id]) end end end