From c0660180b33c1c296e8718d83fc91c9e926124fc Mon Sep 17 00:00:00 2001 From: Rabbit Date: Mon, 18 Mar 2024 15:13:18 +0800 Subject: [PATCH] Feat/rgb (#1682) * refactor: search live cells with bitcoin address * refactor: get address info with bitcoin address * chore: adjust test --- .../api/v1/address_live_cells_controller.rb | 38 +++---------- .../api/v1/address_transactions_controller.rb | 1 + .../api/v1/addresses_controller.rb | 32 ++--------- app/interactions/addresses/info.rb | 22 ++++++++ app/interactions/addresses/live_cells.rb | 37 ++++++++++++ .../addresses/pending_transactions.rb | 48 +++++++--------- app/models/address.rb | 6 +- app/models/null_address.rb | 4 ++ app/serializers/address_serializer.rb | 3 + .../api/v1/addresses_controller_test.rb | 56 ++++++++++--------- .../api/v1/suggest_queries_controller_test.rb | 2 +- 11 files changed, 138 insertions(+), 111 deletions(-) create mode 100644 app/interactions/addresses/info.rb create mode 100644 app/interactions/addresses/live_cells.rb diff --git a/app/controllers/api/v1/address_live_cells_controller.rb b/app/controllers/api/v1/address_live_cells_controller.rb index b4832faf1..f1dc08970 100644 --- a/app/controllers/api/v1/address_live_cells_controller.rb +++ b/app/controllers/api/v1/address_live_cells_controller.rb @@ -1,39 +1,17 @@ module Api module V1 class AddressLiveCellsController < ApplicationController - before_action :validate_pagination_params, :pagination_params + before_action :validate_pagination_params def show - expires_in 1.minutes, public: true, must_revalidate: true, stale_while_revalidate: 10.seconds + expires_in 1.minute, public: true, must_revalidate: true, stale_while_revalidate: 10.seconds - 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] + json = Addresses::LiveCells.run!( + { request:, + key: params[:id], sort: params[:sort], + page: params[:page], page_size: params[:page_size] }, + ) + render json: end end end diff --git a/app/controllers/api/v1/address_transactions_controller.rb b/app/controllers/api/v1/address_transactions_controller.rb index 12aac9458..7ff785927 100644 --- a/app/controllers/api/v1/address_transactions_controller.rb +++ b/app/controllers/api/v1/address_transactions_controller.rb @@ -5,6 +5,7 @@ class AddressTransactionsController < ApplicationController def show expires_in 10.seconds, public: true, must_revalidate: true, stale_while_revalidate: 5.seconds + json = Addresses::CkbTransactions.run!( { request:, key: params[:id], sort: params[:sort], diff --git a/app/controllers/api/v1/addresses_controller.rb b/app/controllers/api/v1/addresses_controller.rb index fab3c69c9..9f3c7fecf 100644 --- a/app/controllers/api/v1/addresses_controller.rb +++ b/app/controllers/api/v1/addresses_controller.rb @@ -1,34 +1,14 @@ module Api module V1 class AddressesController < ApplicationController - before_action :validate_query_params - def show - address = Address.find_address!(params[:id]) - - render json: json_response(address) - end - - private - - def validate_query_params - validator = Validations::Address.new(params) - - if validator.invalid? - errors = validator.error_object[:errors] - status = validator.error_object[:status] - - render json: errors, status: - end - end + expires_in 1.minute, public: true, must_revalidate: true, stale_while_revalidate: 10.seconds - def json_response(address) - if QueryKeyUtils.valid_hex?(params[:id]) - LockHashSerializer.new(address) - else - presented_address = address.is_a?(NullAddress) ? NullAddress.new(params[:id]) : address - AddressSerializer.new(presented_address) - end + json = Addresses::Info.run!( + { request:, key: params[:id], + page: params[:page], page_size: params[:page_size] }, + ) + render json: end end end diff --git a/app/interactions/addresses/info.rb b/app/interactions/addresses/info.rb new file mode 100644 index 000000000..bfaec06ca --- /dev/null +++ b/app/interactions/addresses/info.rb @@ -0,0 +1,22 @@ +module Addresses + class Info < ActiveInteraction::Base + include Api::V1::Exceptions + + object :request, class: ActionDispatch::Request + string :key, default: nil + integer :page, default: 1 + integer :page_size, default: Address.default_per_page + + def execute + address = Explore.run!(key:) + raise AddressNotFoundError if address.is_a?(NullAddress) + + return LockHashSerializer.new(address[0]) if QueryKeyUtils.valid_hex?(key) + + options = FastJsonapi::PaginationMetaGenerator.new( + request:, records: address, page:, page_size:, total_count: address.count, + ).call + AddressSerializer.new(address, options) + end + end +end diff --git a/app/interactions/addresses/live_cells.rb b/app/interactions/addresses/live_cells.rb new file mode 100644 index 000000000..5e6815eb6 --- /dev/null +++ b/app/interactions/addresses/live_cells.rb @@ -0,0 +1,37 @@ +module Addresses + class LiveCells < ActiveInteraction::Base + include Api::V1::Exceptions + + object :request, class: ActionDispatch::Request + string :key, default: nil + string :sort, default: "block_timestamp.desc" + integer :page, default: 1 + integer :page_size, default: CellOutput.default_per_page + + def execute + address = Explore.run!(key:) + raise AddressNotFoundError if address.is_a?(NullAddress) + + order_by, asc_or_desc = live_cells_ordering + records = CellOutput.live.where(address_id: address.map(&:id)). + order(order_by => asc_or_desc). + page(page).per(page_size).fast_page + options = FastJsonapi::PaginationMetaGenerator.new( + request:, records:, page:, page_size:, + ).call + + CellOutputSerializer.new(records, options).serialized_json + end + + private + + def live_cells_ordering + sort_by, sort_order = sort.split(".", 2) + if sort_order.nil? || !sort_order.match?(/^(asc|desc)$/i) + sort_order = "asc" + end + + [sort_by, sort_order] + end + end +end diff --git a/app/interactions/addresses/pending_transactions.rb b/app/interactions/addresses/pending_transactions.rb index 51a9e3f54..49aad2b40 100644 --- a/app/interactions/addresses/pending_transactions.rb +++ b/app/interactions/addresses/pending_transactions.rb @@ -12,25 +12,24 @@ def execute address = Explore.run!(key:) raise AddressNotFoundError if address.is_a?(NullAddress) - address_id = address.map(&:id) - - order_by, asc_or_desc = account_books_ordering account_books = - AccountBook.joins(:ckb_transaction). - where(account_books: { address_id: }, - ckb_transactions: { tx_status: "pending" }). - order(order_by => asc_or_desc). - page(page).per(page_size).fast_page - - ckb_transaction_ids = account_books.map(&:ckb_transaction_id) - ckb_transaction_ids = CellInput.where(ckb_transaction_id: ckb_transaction_ids). - where.not(previous_cell_output_id: nil, from_cell_base: false). - distinct.pluck(:ckb_transaction_id) - records = CkbTransaction.where(id: ckb_transaction_ids). - select(select_fields). - order(order_by => asc_or_desc) + AccountBook.joins(:ckb_transaction).where( + account_books: { address_id: address.map(&:id) }, + ckb_transactions: { tx_status: "pending" }, + ) + ckb_transaction_ids = + CellInput.where(ckb_transaction_id: account_books.map(&:ckb_transaction_id)). + where.not(previous_cell_output_id: nil, from_cell_base: false). + distinct.pluck(:ckb_transaction_id) + records = + CkbTransaction.where(id: ckb_transaction_ids). + select(select_fields). + order(transactions_ordering). + page(page).per(page_size) - options = paginate_options(records, address_id) + options = FastJsonapi::PaginationMetaGenerator.new( + request:, records:, page:, page_size:, + ).call options.merge!(params: { previews: true, address: }) result = CkbTransactionsSerializer.new(records, options) @@ -39,26 +38,19 @@ def execute private - def account_books_ordering + def transactions_ordering sort_by, sort_order = sort.split(".", 2) sort_by = case sort_by - when "time" then "ckb_transactions.block_timestamp" - else "ckb_transactions.id" + when "time" then "block_timestamp" + else "id" end if sort_order.nil? || !sort_order.match?(/^(asc|desc)$/i) sort_order = "asc" end - [sort_by, sort_order] - end - - def paginate_options(records, address_id) - total_count = AccountBook.where(address_id:).count - FastJsonapi::PaginationMetaGenerator.new( - request:, records:, page:, page_size:, total_count:, - ).call + "#{sort_by} #{sort_order} NULLS LAST" end def select_fields diff --git a/app/models/address.rb b/app/models/address.rb index ef36f224c..d207e27b8 100644 --- a/app/models/address.rb +++ b/app/models/address.rb @@ -1,6 +1,10 @@ class Address < ApplicationRecord PREFIX_MAINNET = "ckb".freeze PREFIX_TESTNET = "ckt".freeze + MAX_PAGINATES_PER = 1000 + DEFAULT_PAGINATES_PER = 100 + paginates_per DEFAULT_PAGINATES_PER + max_paginates_per MAX_PAGINATES_PER has_many :cell_outputs, dependent: :destroy has_many :account_books, dependent: :destroy @@ -9,7 +13,7 @@ class Address < ApplicationRecord has_many :udt_accounts has_many :dao_events - has_one :bitcoin_address_mapping + has_one :bitcoin_address_mapping, foreign_key: "ckb_address_id" has_one :bitcoin_address, through: :bitcoin_address_mapping validates :balance, :cell_consumed, :ckb_transactions_count, :interest, :dao_deposit, diff --git a/app/models/null_address.rb b/app/models/null_address.rb index f5fbaf2d2..aa521049b 100644 --- a/app/models/null_address.rb +++ b/app/models/null_address.rb @@ -71,6 +71,10 @@ def balance_occupied 0 end + def bitcoin_address + nil + end + alias_method :cached_lock_script, :lock_script alias_method :query_address, :address_hash end diff --git a/app/serializers/address_serializer.rb b/app/serializers/address_serializer.rb index f42bcec68..2034a4e02 100644 --- a/app/serializers/address_serializer.rb +++ b/app/serializers/address_serializer.rb @@ -131,4 +131,7 @@ class AddressSerializer attribute :balance_occupied do |object| object.balance_occupied.to_s end + attribute :bitcoin_address_hash do |object| + object.bitcoin_address&.address_hash + end end diff --git a/test/controllers/api/v1/addresses_controller_test.rb b/test/controllers/api/v1/addresses_controller_test.rb index f8e2ff670..f1ec31369 100644 --- a/test/controllers/api/v1/addresses_controller_test.rb +++ b/test/controllers/api/v1/addresses_controller_test.rb @@ -64,7 +64,7 @@ class AddressesControllerTest < ActionDispatch::IntegrationTest end test "should return error object when id is not a address hash" do - error_object = Api::V1::Exceptions::AddressHashInvalidError.new + error_object = Api::V1::Exceptions::AddressNotFoundError.new response_json = RequestErrorSerializer.new([error_object], message: error_object.title).serialized_json @@ -74,12 +74,18 @@ class AddressesControllerTest < ActionDispatch::IntegrationTest end test "should return corresponding data with given address hash" do + page = 1 + page_size = 100 address = create(:address, :with_lock_script) address.query_address = address.address_hash valid_get api_v1_address_url(address.address_hash) - assert_equal AddressSerializer.new(address).serialized_json, + options = FastJsonapi::PaginationMetaGenerator.new( + request:, records: [address], page:, page_size:, total_count: 1, + ).call + + assert_equal AddressSerializer.new([address], options).serialized_json, response.body end @@ -97,19 +103,11 @@ class AddressesControllerTest < ActionDispatch::IntegrationTest valid_get api_v1_address_url(address.address_hash) - assert_equal %w(address_hash balance transactions_count lock_script dao_deposit interest lock_info is_special live_cells_count mined_blocks_count average_deposit_time udt_accounts dao_compensation balance_occupied).sort, - json["data"]["attributes"].keys.sort - end - - test "should return NullAddress when address no found by id" do - ENV["CKB_NET_MODE"] = "testnet" - address = NullAddress.new("ckt1qyqrdsefa43s6m882pcj53m4gdnj4k440axqswmu83") - response_json = AddressSerializer.new(address).serialized_json - - valid_get api_v1_address_url("ckt1qyqrdsefa43s6m882pcj53m4gdnj4k440axqswmu83") - - assert_equal response_json, response.body - ENV["CKB_NET_MODE"] = "mainnet" + assert_equal %w(address_hash balance transactions_count lock_script dao_deposit + interest lock_info is_special live_cells_count mined_blocks_count + average_deposit_time udt_accounts dao_compensation balance_occupied + bitcoin_address_hash).sort, + json["data"][0]["attributes"].keys.sort end test "should return special address when query address is special" do @@ -118,7 +116,7 @@ class AddressesControllerTest < ActionDispatch::IntegrationTest valid_get api_v1_address_url(address.address_hash) assert_equal Settings.special_addresses[address.address_hash], - json.dig("data", "attributes", "special_address") + json.dig("data", 0, "attributes", "special_address") end test "should not return special address when query address is not special" do @@ -126,7 +124,7 @@ class AddressesControllerTest < ActionDispatch::IntegrationTest address_hash: "ckb1qyqdmeuqrsrnm7e5vnrmruzmsp4m9wacf6vsxasryq") valid_get api_v1_address_url(address.address_hash) - assert_nil json.dig("data", "attributes", "special_address") + assert_nil json.dig("data", 0, "attributes", "special_address") end test "should support full address query when short address's lock script exists" do @@ -136,7 +134,11 @@ class AddressesControllerTest < ActionDispatch::IntegrationTest address.query_address = query_key valid_get api_v1_address_url(query_key) - assert_equal AddressSerializer.new(address).serialized_json, + options = FastJsonapi::PaginationMetaGenerator.new( + request:, records: [address], page: 1, page_size: 100, total_count: 1, + ).call + + assert_equal AddressSerializer.new([address], options).serialized_json, response.body end @@ -147,7 +149,11 @@ class AddressesControllerTest < ActionDispatch::IntegrationTest address.query_address = query_key valid_get api_v1_address_url(query_key) - assert_equal AddressSerializer.new(address).serialized_json, + options = FastJsonapi::PaginationMetaGenerator.new( + request:, records: [address], page: 1, page_size: 100, total_count: 1, + ).call + + assert_equal AddressSerializer.new([address], options).serialized_json, response.body end @@ -168,7 +174,7 @@ class AddressesControllerTest < ActionDispatch::IntegrationTest "display_name" => nil, "uan" => nil, }, - ], json.dig("data", "attributes", "udt_accounts") + ], json.dig("data", 0, "attributes", "udt_accounts") end test "should not return unpublished udt accounts with given address hash" do @@ -178,7 +184,7 @@ class AddressesControllerTest < ActionDispatch::IntegrationTest valid_get api_v1_address_url(address.address_hash) - assert_empty json.dig("data", "attributes", "udt_accounts") + assert_empty json.dig("data", 0, "attributes", "udt_accounts") end test "should return balance occupied" do @@ -186,7 +192,7 @@ class AddressesControllerTest < ActionDispatch::IntegrationTest address_hash: "ckb1qyq0hcfpff4h8w8zvy44uurvlgdrr09tefwqx266dl") valid_get api_v1_address_url(address.address_hash) - assert_equal "0", json.dig("data", "attributes", "balance_occupied") + assert_equal "0", json.dig("data", 0, "attributes", "balance_occupied") end test "should return nrc 721 udt accounts with given address hash" do @@ -216,7 +222,7 @@ class AddressesControllerTest < ActionDispatch::IntegrationTest "udt_type" => udt_account.udt_type, "collection" => { "type_hash" => type_script.script_hash }, }, - ], json.dig("data", "attributes", "udt_accounts") + ], json.dig("data", 0, "attributes", "udt_accounts") end test "should return spore cell udt accounts with given address hash" do @@ -242,7 +248,7 @@ class AddressesControllerTest < ActionDispatch::IntegrationTest "udt_type" => udt_account.udt_type, "collection" => { "type_hash" => cluster_type.script_hash }, }, - ], json.dig("data", "attributes", "udt_accounts") + ], json.dig("data", 0, "attributes", "udt_accounts") end test "should return omiga inscription udt accounts with given address hash" do @@ -265,7 +271,7 @@ class AddressesControllerTest < ActionDispatch::IntegrationTest "expected_supply" => info.expected_supply.to_s, "mint_status" => info.mint_status, }, - ], json.dig("data", "attributes", "udt_accounts") + ], json.dig("data", 0, "attributes", "udt_accounts") end end end diff --git a/test/controllers/api/v1/suggest_queries_controller_test.rb b/test/controllers/api/v1/suggest_queries_controller_test.rb index cc1b4e7ad..95126a02a 100644 --- a/test/controllers/api/v1/suggest_queries_controller_test.rb +++ b/test/controllers/api/v1/suggest_queries_controller_test.rb @@ -162,7 +162,7 @@ class SuggestQueriesControllerTest < ActionDispatch::IntegrationTest address = create(:address, :with_lock_script, address_hash: "ckb1qjda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xw3vumhs9nvu786dj9p0q5elx66t24n3kxgj53qks") query_key = "ckb1qyqt8xaupvm8837nv3gtc9x0ekkj64vud3jqfwyw5v" address.query_address = query_key - valid_get api_v1_address_url(query_key) + valid_get api_v1_suggest_queries_url, params: { q: query_key } assert_equal AddressSerializer.new(address).serialized_json, response.body end