Skip to content

Commit

Permalink
fix: add portfolio request filter (#1609)
Browse files Browse the repository at this point in the history
* fix: add portfolio request filter

* test: adjust test
  • Loading branch information
rabbitz authored Jan 30, 2024
1 parent 2f318c1 commit 06d070d
Show file tree
Hide file tree
Showing 14 changed files with 200 additions and 119 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,5 @@ gem "kredis"
gem "async-websocket", "~> 0.22.1", require: false
gem "ecdsa"
gem "jwt"

gem "active_interaction", "~> 5.3"
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -475,6 +478,7 @@ PLATFORMS
x86_64-linux

DEPENDENCIES
active_interaction (~> 5.3)
activerecord-import
after_commit_everywhere
annotate
Expand Down
16 changes: 12 additions & 4 deletions app/controllers/api/v2/base_controller.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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!
Expand Down
61 changes: 4 additions & 57 deletions app/controllers/api/v2/portfolio/ckb_transactions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
21 changes: 4 additions & 17 deletions app/controllers/api/v2/portfolio/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 3 additions & 28 deletions app/controllers/api/v2/portfolio/statistics_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/api/v2/portfolio/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions app/controllers/validations/portfolio_signature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions app/interactions/users/ckb_transactions.rb
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions app/interactions/users/sign_in.rb
Original file line number Diff line number Diff line change
@@ -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
38 changes: 38 additions & 0 deletions app/interactions/users/statistics.rb
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions app/interactions/users/update.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 06d070d

Please sign in to comment.