diff --git a/Gemfile b/Gemfile index 0cb8bc32d..335fe13c7 100644 --- a/Gemfile +++ b/Gemfile @@ -125,3 +125,5 @@ gem "kredis" gem "async-websocket", "~> 0.22.1", require: false gem "ecdsa" gem "jwt" + +gem "active_interaction", "~> 5.3" diff --git a/Gemfile.lock b/Gemfile.lock index 81d127338..779347085 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -55,6 +55,9 @@ GEM erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) + active_interaction (5.3.0) + activemodel (>= 5.2, < 8) + activesupport (>= 5.2, < 8) activejob (7.0.4) activesupport (= 7.0.4) globalid (>= 0.3.6) @@ -475,6 +478,7 @@ PLATFORMS x86_64-linux DEPENDENCIES + active_interaction (~> 5.3) activerecord-import after_commit_everywhere annotate 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/controllers/api/v2/base_controller.rb b/app/controllers/api/v2/base_controller.rb index 918f64233..7c14d0f8e 100644 --- a/app/controllers/api/v2/base_controller.rb +++ b/app/controllers/api/v2/base_controller.rb @@ -1,10 +1,22 @@ module Api module V2 class BaseController < ActionController::API + wrap_parameters false + include Pagy::Backend + rescue_from ActiveInteraction::InvalidInteractionError, with: :handle_params_error rescue_from Api::V2::Exceptions::Error, with: :api_error + def handle_params_error(error) + error = Api::V2::Exceptions::ParamsInvalidError.new(error.message.squish.to_s) + api_error(error) + end + + def api_error(error) + render json: RequestErrorSerializer.new([error], message: error.title), status: error.status + end + def address_to_lock_hash(address) if address.start_with?("0x") address @@ -19,10 +31,6 @@ def pagy_get_items(collection, pagy) collection.offset(pagy.offset).limit(pagy.items).fast_page end - def api_error(error) - render json: RequestErrorSerializer.new([error], message: error.title), status: error.status - end - attr_reader :current_user def validate_jwt! diff --git a/app/controllers/api/v2/portfolio/ckb_transactions_controller.rb b/app/controllers/api/v2/portfolio/ckb_transactions_controller.rb index 49b06a017..e798d1418 100644 --- a/app/controllers/api/v2/portfolio/ckb_transactions_controller.rb +++ b/app/controllers/api/v2/portfolio/ckb_transactions_controller.rb @@ -3,33 +3,12 @@ module V2 module Portfolio class CkbTransactionsController < BaseController before_action :validate_jwt! - before_action :pagination_params def index expires_in 15.minutes, public: true, stale_while_revalidate: 5.minutes, stale_if_error: 5.minutes + json = Users::CkbTransactions.run!(transaction_list_params.merge({ user: current_user, request: })) - account_books = sort_account_books(filter_account_books).page(@page).per(@page_size).fast_page - ckb_transactions = CkbTransaction.where(id: account_books.map(&:ckb_transaction_id)). - select(:id, :tx_hash, :block_id, :block_number, :block_timestamp, - :is_cellbase, :updated_at, :capacity_involved). - order(id: :desc) - options = FastJsonapi::PaginationMetaGenerator.new( - request: request, - records: ckb_transactions, - page: @page, - page_size: @page_size, - records_counter: account_books - ).call - ckb_transaction_serializer = CkbTransactionsSerializer.new( - ckb_transactions, - options.merge(params: { - previews: true, - address: current_user.addresses - }) - ) - json = ckb_transaction_serializer.serialized_json - - render json: json + render json: end def download_csv @@ -42,40 +21,8 @@ def download_csv private - def pagination_params - @page = params[:page] || 1 - @page_size = params[:page_size] || CkbTransaction.default_per_page - end - - def filter_account_books - address_ids = - if params[:address_hash].present? - address = Address.find_address!(params[:address_hash]) - [address.id] - else - current_user.address_ids - end - scope = AccountBook.joins(:ckb_transaction).where( - account_books: { address_id: address_ids }, - ckb_transactions: { tx_status: "committed" } - ) - - if params[:tx_hash].present? - scope = scope.where(ckb_transactions: { tx_hash: params[:tx_hash] }) - end - - scope - end - - def sort_account_books(records) - sort, order = params.fetch(:sort, "ckb_transaction_id.desc").split(".", 2) - sort = "ckb_transactions.block_timestamp" if sort == "time" - - if order.nil? || !order.match?(/^(asc|desc)$/i) - order = "asc" - end - - records.order("#{sort} #{order}") + def transaction_list_params + params.permit(:address_hash, :tx_hash, :sort, :page, :page_size) end def download_params diff --git a/app/controllers/api/v2/portfolio/sessions_controller.rb b/app/controllers/api/v2/portfolio/sessions_controller.rb index 12598576b..514ae466d 100644 --- a/app/controllers/api/v2/portfolio/sessions_controller.rb +++ b/app/controllers/api/v2/portfolio/sessions_controller.rb @@ -2,29 +2,16 @@ module Api module V2 module Portfolio class SessionsController < BaseController - before_action :validate_query_params - def create - user = User.find_or_create_by(identifier: params[:address]) - payload = { uuid: user.uuid } + json = Users::SignIn.run!(sign_in_params) - render json: { - name: user.name, - jwt: PortfolioUtils.generate_jwt(payload) - } + render json: end private - def validate_query_params - validator = Validations::PortfolioSignature.new(params) - - if validator.invalid? - errors = validator.error_object[:errors] - status = validator.error_object[:status] - - render json: errors, status: status - end + def sign_in_params + params.permit(:address, :message, :signature, :pub_key) end end end diff --git a/app/controllers/api/v2/portfolio/statistics_controller.rb b/app/controllers/api/v2/portfolio/statistics_controller.rb index 15a6b3301..4aea6c7d5 100644 --- a/app/controllers/api/v2/portfolio/statistics_controller.rb +++ b/app/controllers/api/v2/portfolio/statistics_controller.rb @@ -2,37 +2,12 @@ module Api module V2 module Portfolio class StatisticsController < BaseController - before_action :validate_jwt!, :check_addresses_consistent! + before_action :validate_jwt! def index expires_in 30.minutes, public: true, stale_while_revalidate: 10.minutes, stale_if_error: 10.minutes - - addresses = current_user.addresses - balance = addresses.pluck(:balance).sum - balance_occupied = addresses.pluck(:balance_occupied).sum - dao_deposit = addresses.pluck(:dao_deposit).sum - interest = addresses.pluck(:interest).sum - unclaimed_compensation = addresses.pluck(:unclaimed_compensation).sum - - json = { - balance: balance.to_s, - balance_occupied: balance_occupied.to_s, - dao_deposit: dao_deposit.to_s, - interest: interest.to_s, - dao_compensation: (interest.to_i + unclaimed_compensation.to_i).to_s - } - - render json: { data: json } - end - - private - - def check_addresses_consistent! - address = Address.find_by_address_hash(params[:latest_address]) - unless current_user.portfolios.exists?(address: address) - latest_address = current_user.portfolios.last&.address - raise Api::V2::Exceptions::PortfolioLatestDiscrepancyError.new(latest_address&.address_hash) - end + data = Users::Statistics.run!({ user: current_user, latest_address: params[:latest_address] }) + render json: { data: } end end end diff --git a/app/controllers/api/v2/portfolio/users_controller.rb b/app/controllers/api/v2/portfolio/users_controller.rb index 63f1da811..da76860b4 100644 --- a/app/controllers/api/v2/portfolio/users_controller.rb +++ b/app/controllers/api/v2/portfolio/users_controller.rb @@ -5,7 +5,7 @@ class UsersController < BaseController before_action :validate_jwt! def update - current_user.update(name: params[:name]) + Users::Update.run!({ user: current_user, name: params[:name] }) head :no_content end diff --git a/app/controllers/api/v2/scripts_controller.rb b/app/controllers/api/v2/scripts_controller.rb index c9835741e..4d20ab7b2 100644 --- a/app/controllers/api/v2/scripts_controller.rb +++ b/app/controllers/api/v2/scripts_controller.rb @@ -3,8 +3,8 @@ module Api module V2 class ScriptsController < BaseController - before_action :set_page_and_page_size - before_action :find_script + before_action :set_page_and_page_size, except: :referring_capacities + before_action :find_script, except: :referring_capacities def general_info head :not_found and return if @script.blank? || @contract.blank? @@ -49,6 +49,13 @@ def referring_cells end end + def referring_capacities + expires_in 15.seconds, public: true, must_revalidate: true, stale_while_revalidate: 5.seconds + data = Contract.all.map { { _1.code_hash => _1.total_referring_cells_capacity.to_s } } + + render json: { data: } + end + private def get_script_content @@ -69,7 +76,7 @@ def get_script_content capacity_of_referring_cells: @contract.total_referring_cells_capacity, count_of_transactions: @contract.ckb_transactions_count, count_of_deployed_cells: @contract.deployed_cells_count, - count_of_referring_cells: @contract.referring_cells_count + count_of_referring_cells: @contract.referring_cells_count, } end diff --git a/app/controllers/validations/portfolio_signature.rb b/app/controllers/validations/portfolio_signature.rb index 1af5708e3..e3c9c27c6 100644 --- a/app/controllers/validations/portfolio_signature.rb +++ b/app/controllers/validations/portfolio_signature.rb @@ -8,10 +8,10 @@ class PortfolioSignature validate :signature_must_be_valid def initialize(params = {}) - @address = params[:address] - @message = params[:message] - @signature = params[:signature] - @pub_key = params[:pub_key] + @address = params[:address].to_s + @message = params[:message].to_s + @signature = params[:signature].to_s + @pub_key = params[:pub_key].to_s end def error_object diff --git a/app/interactions/users/ckb_transactions.rb b/app/interactions/users/ckb_transactions.rb new file mode 100644 index 000000000..b9842ad7f --- /dev/null +++ b/app/interactions/users/ckb_transactions.rb @@ -0,0 +1,64 @@ +module Users + class CkbTransactions < ActiveInteraction::Base + include Api::V2::Exceptions + + object :user + object :request, class: ActionDispatch::Request + string :address_hash, default: nil + string :tx_hash, default: nil + string :sort, default: "ckb_transaction_id.desc" + integer :page, default: 1 + integer :page_size, default: CkbTransaction.default_per_page + + def execute + account_books = sort_account_books(filter_account_books).page(page).per(page_size).fast_page + transactions = CkbTransaction.where(id: account_books.map(&:ckb_transaction_id)). + select(:id, :tx_hash, :block_id, :block_number, :block_timestamp, + :is_cellbase, :updated_at, :capacity_involved). + order(id: :desc) + + options = FastJsonapi::PaginationMetaGenerator.new( + records: transactions, + records_counter: account_books, + request:, + page:, + page_size:, + ).call + options[:params] = { previews: true, address: user.addresses } + + transactions_serializer = CkbTransactionsSerializer.new(transactions, options) + transactions_serializer.serialized_json + end + + private + + def filter_account_books + address_ids = user.address_ids + if address_hash.present? + address = Address.find_address!(address_hash) + address_ids = Array[address.id] + end + + scope = AccountBook.joins(:ckb_transaction).where( + account_books: { address_id: address_ids }, + ckb_transactions: { tx_status: "committed" }, + ) + scope = scope.where(ckb_transactions: { tx_hash: }) if tx_hash.present? + + scope + rescue StandardError + raise AddressNotFoundError.new + end + + def sort_account_books(records) + sorting, ordering = sort.split(".", 2) + sorting = "ckb_transactions.block_timestamp" if sorting == "time" + + if ordering.nil? || !ordering.match?(/^(asc|desc)$/i) + ordering = "asc" + end + + records.order("#{sorting} #{ordering}") + end + end +end diff --git a/app/interactions/users/sign_in.rb b/app/interactions/users/sign_in.rb new file mode 100644 index 000000000..124197807 --- /dev/null +++ b/app/interactions/users/sign_in.rb @@ -0,0 +1,32 @@ +module Users + class SignIn < ActiveInteraction::Base + include Api::V2::Exceptions + + string :address, :message, :signature + string :pub_key, default: nil + + validates :address, :message, :signature, presence: true + validate :validate_params_format! + validate :validate_signature! + + def execute + user = User.find_or_create_by(identifier: address) + jwt = PortfolioUtils.generate_jwt({ uuid: user.uuid }) + + { name: user.name, jwt: } + end + + private + + def validate_params_format! + raise AddressNotMatchEnvironmentError.new(ENV["CKB_NET_MODE"]) unless QueryKeyUtils.valid_address?(address) + raise InvalidPortfolioMessageError.new unless QueryKeyUtils.hex_string?(message) + raise InvalidPortfolioSignatureError.new unless QueryKeyUtils.hex_string?(signature) + end + + def validate_signature! + verified = PortfolioSignatureVerifier.new(address, message, signature, pub_key).verified? + raise InvalidPortfolioSignatureError.new unless verified + end + end +end diff --git a/app/interactions/users/statistics.rb b/app/interactions/users/statistics.rb new file mode 100644 index 000000000..60051e30b --- /dev/null +++ b/app/interactions/users/statistics.rb @@ -0,0 +1,38 @@ +module Users + class Statistics < ActiveInteraction::Base + include Api::V2::Exceptions + + object :user + string :latest_address + + validates :latest_address, presence: true + validate :check_addresses_consistent! + + def execute + addresses = user.addresses + balance = addresses.pluck(:balance).sum + balance_occupied = addresses.pluck(:balance_occupied).sum + dao_deposit = addresses.pluck(:dao_deposit).sum + interest = addresses.pluck(:interest).sum + unclaimed_compensation = addresses.pluck(:unclaimed_compensation).sum + dao_compensation = interest.to_i + unclaimed_compensation.to_i + + CkbUtils.hash_value_to_s(balance:, balance_occupied:, dao_deposit:, + interest:, dao_compensation:) + end + + private + + def check_addresses_consistent! + unless QueryKeyUtils.valid_address?(latest_address) + raise AddressNotMatchEnvironmentError.new(ENV["CKB_NET_MODE"]) + end + + address = Address.find_by_address_hash(latest_address) + unless user.portfolios.exists?(address:) + address = user.portfolios.last&.address + raise PortfolioLatestDiscrepancyError.new(address&.address_hash) + end + end + end +end diff --git a/app/interactions/users/update.rb b/app/interactions/users/update.rb new file mode 100644 index 000000000..e6abec754 --- /dev/null +++ b/app/interactions/users/update.rb @@ -0,0 +1,12 @@ +module Users + class Update < ActiveInteraction::Base + object :user + string :name + + validates :name, presence: true + + def execute + user.update(name:) + end + end +end diff --git a/app/lib/api/v2/exceptions.rb b/app/lib/api/v2/exceptions.rb index 2a6d1223f..857785075 100644 --- a/app/lib/api/v2/exceptions.rb +++ b/app/lib/api/v2/exceptions.rb @@ -13,6 +13,12 @@ def initialize(code:, status:, title:, detail:, href:) end end + class ParamsInvalidError < Error + def initialize(detail) + super(code: 2000, status: 404, title: "Params are invalid", detail:, href:) + end + end + class TokenCollectionNotFoundError < Error def initialize super(code: 2001, status: 404, title: "Token Collection Not Found", detail: "No token collection found by given script hash or id", href: "") @@ -39,19 +45,19 @@ def initialize class UserNotExistError < Error def initialize(detail) - super(code: 2005, status: 400, title: "user not exist", detail: detail, href: "") + super(code: 2005, status: 400, title: "user not exist", detail:, href: "") end end class DecodeJWTFailedError < Error def initialize(detail) - super(code: 2006, status: 400, title: "decode JWT failed", detail: detail, href: "") + super(code: 2006, status: 400, title: "decode JWT failed", detail:, href: "") end end class PortfolioLatestDiscrepancyError < Error def initialize(detail) - super(code: 2007, status: 400, title: "portfolio has not synchronized the latest addresses", detail: "", href: "") + super(code: 2007, status: 400, title: "portfolio has not synchronized the latest addresses", detail:, href: "") end end @@ -60,6 +66,12 @@ def initialize super(code: 2008, status: 400, title: "sync portfolio addresses failed", detail: "", href: "") end end + + class AddressNotFoundError < Error + def initialize + super code: 2009, status: 404, title: "Address Not Found", detail: "No address found by given address hash or lock hash", href: "" + end + end end end end diff --git a/app/models/cell_output.rb b/app/models/cell_output.rb index 72950988c..691763982 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.full_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/config/routes/v2.rb b/config/routes/v2.rb index c48e88196..96dad4698 100644 --- a/config/routes/v2.rb +++ b/config/routes/v2.rb @@ -1,7 +1,7 @@ namespace :api do namespace :v2 do post "/das_accounts" => "das_accounts#query", as: :das_accounts - resources :ckb_transactions, only: [:index, :show] do + resources :ckb_transactions, only: %i[index show] do member do get :details get :display_inputs @@ -27,7 +27,7 @@ resources :holders, only: :index resources :transfers, only: :index resources :items do - resources :transfers, only: [:index, :show] + resources :transfers, only: %i[index show] end end namespace :cota do @@ -47,7 +47,7 @@ end end resources :items, only: :index - resources :transfers, only: [:index, :show] do + resources :transfers, only: %i[index show] do collection do get :download_csv end @@ -61,6 +61,7 @@ get :deployed_cells get :referring_cells get :general_info + get :referring_capacities end end 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/controllers/api/v2/portfolio/statistics_controller_test.rb b/test/controllers/api/v2/portfolio/statistics_controller_test.rb index 7acf09600..bc39e2c2a 100644 --- a/test/controllers/api/v2/portfolio/statistics_controller_test.rb +++ b/test/controllers/api/v2/portfolio/statistics_controller_test.rb @@ -13,7 +13,7 @@ class StatisticsControllerTest < ActionDispatch::IntegrationTest test "should respond with error object when addresses inconsistencies detected" do address = "ckt1q3w9q60tppt7l3j7r09qcp7lxnp3vcanvgha8pmvsa3jplykxn323k5v49yzmvm0q0kfqw0hk0kyal6z32nwjvcqqr7qyzq8yqtec2wj" - error_object = Api::V2::Exceptions::PortfolioLatestDiscrepancyError.new(address) + error_object = Api::V2::Exceptions::PortfolioLatestDiscrepancyError.new(nil) response_json = RequestErrorSerializer.new([error_object], message: error_object.title).serialized_json get api_v2_portfolio_statistics_url(latest_address: address), @@ -23,8 +23,8 @@ class StatisticsControllerTest < ActionDispatch::IntegrationTest test "should return statistic when address inconsistencies resolved" do address_hash = "ckt1q3w9q60tppt7l3j7r09qcp7lxnp3vcanvgha8pmvsa3jplykxn323k5v49yzmvm0q0kfqw0hk0kyal6z32nwjvcqqr7qyzq8yqtec2wj" - address = create(:address, address_hash: address_hash) - create(:portfolio, user: @user, address: address) + address = create(:address, address_hash:) + create(:portfolio, user: @user, address:) response_json = { data: { @@ -32,8 +32,8 @@ class StatisticsControllerTest < ActionDispatch::IntegrationTest balance_occupied: address.balance_occupied.to_s, dao_deposit: address.dao_deposit.to_s, interest: address.interest.to_s, - dao_compensation: (address.interest.to_i + address.unclaimed_compensation.to_i).to_s - } + dao_compensation: (address.interest.to_i + address.unclaimed_compensation.to_i).to_s, + }, }.as_json get api_v2_portfolio_statistics_url(latest_address: address_hash), headers: { diff --git a/test/controllers/api/v2/scripts_controller_test.rb b/test/controllers/api/v2/scripts_controller_test.rb index 81d6aa090..1423bd797 100644 --- a/test/controllers/api/v2/scripts_controller_test.rb +++ b/test/controllers/api/v2/scripts_controller_test.rb @@ -4,8 +4,8 @@ module Api module V2 class ScriptsControllerTest < ActionDispatch::IntegrationTest setup do - @code_hash = '0x00000000000000000000000000000000000000000000000000545950455f4944' - @hash_type = 'type' + @code_hash = "0x00000000000000000000000000000000000000000000000000545950455f4944" + @hash_type = "type" @block = create :block @contract = create :contract, code_hash: @code_hash, hash_type: @hash_type @script = create :script, contract_id: @contract.id @@ -35,6 +35,14 @@ class ScriptsControllerTest < ActionDispatch::IntegrationTest valid_get referring_cells_api_v2_scripts_url(code_hash: @code_hash, hash_type: @hash_type) assert_response :success end + + test "should get referring_capacities" do + create_list(:contract, 10) + referring_capacities = Contract.all.map { { _1.code_hash => _1.total_referring_cells_capacity.to_s } } + + valid_get referring_capacities_api_v2_scripts_url + assert_equal ({ "data" => referring_capacities }), json + 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 diff --git a/test/factories/contract.rb b/test/factories/contract.rb index a4286f2ed..03c190886 100644 --- a/test/factories/contract.rb +++ b/test/factories/contract.rb @@ -9,6 +9,8 @@ description { "SECP256K1/multisig (Source Code) is a script which allows a group of users to sign a single transaction." } verified { false } deprecated { false } + total_referring_cells_capacity { SecureRandom.random_number(10**10) } + after(:create) do |contract, _eval| tx = create :ckb_transaction, :with_single_output co = tx.cell_outputs.first