Skip to content

Commit

Permalink
feat: add portfolio sign in (#1484)
Browse files Browse the repository at this point in the history
* feat: add portfolio sign in

* feat: update user name

* feat: add user portfolio statistic

* chore: fix test helper

* feat: sync portfolio addresses

* feat: add portfolio statistic

* feat: added sort to portfolio transactions

* chore: refactor filter portfolio account books query

* feat: add download portfolio transactions

* chore: delete portfolio statistic

* chore: adjust tests

* chore: update env test conf

* chore: adjust tests
  • Loading branch information
rabbitz authored Jan 25, 2024
1 parent 1beaa52 commit a8ccc16
Show file tree
Hide file tree
Showing 35 changed files with 1,135 additions and 12 deletions.
12 changes: 8 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# -------------------------------- CKB segment --------------------------------
# -------------------------------- CKB segment --------------------------------
# very important, ckb config items
# mainnet | testnet
CKB_NET_MODE="mainnet"
Expand Down Expand Up @@ -65,7 +65,7 @@ DB_REAP_FREQ=10
MEMCACHED_URL="memcached://ckb-explorer-memcached:11211"


# -------------------------------- 3rd deps segment --------------------------------
# -------------------------------- 3rd deps segment --------------------------------
# default is https://indexer-basic.da.systems/v1/
DAS_INDEXER_URL="https://indexer-basic.da.systems/v1/"

Expand All @@ -77,7 +77,7 @@ STAGING_DOMAIN="https://ckb-explorer.mainnet.layerview.io"
#COTA_AGGREGATOR_URL="http://cota-aggregator:3030"


# -------------------------------- profiling segment --------------------------------
# -------------------------------- profiling segment --------------------------------
# sentry config segment
SENTRY_DSN="https://[email protected]/xxx"
SENTRY_SAMPLE_RATE="1.0"
Expand All @@ -86,7 +86,7 @@ SENTRY_SAMPLE_RATE="1.0"
NEWRELIC_LICENSE_KEY=""


# -------------------------------- misc segment --------------------------------
# -------------------------------- misc segment --------------------------------
# used in statistics_controller
# on or nil
MINER_RANKING_EVENT="on"
Expand All @@ -113,3 +113,7 @@ ASSET_URL=""
# used in Rails test environment, setting to true enables SimpleCov::Formatter::Codecov
# true | false
CI="false"

# -------------------------------- portfolio segment --------------------------------
AUTH_ACCESS_EXPIRE=1296000
SECRET_KEY_BASE=""
6 changes: 5 additions & 1 deletion .env.test.local.travis
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ REDIS_URL="redis://localhost:6379/1"
# side kiq, reaping_frequency, default is 10
DB_REAP_FREQ=10

# -------------------------------- 3rd deps segment --------------------------------
# -------------------------------- 3rd deps segment --------------------------------
# default is https://indexer-basic.da.systems/v1/
DAS_INDEXER_URL="https://indexer-basic.da.systems/v1/"

Expand Down Expand Up @@ -45,3 +45,7 @@ PROPOSAL_WINDOW="10"
# used in ./lib/tasks/migration/register_udt.rake
FORCE_BRIDGE_HOST=""
ASSET_URL=""

# -------------------------------- portfolio segment --------------------------------
AUTH_ACCESS_EXPIRE=1296000
SECRET_KEY_BASE=
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,5 @@ gem "after_commit_everywhere"
gem "kredis"

gem "async-websocket", "~> 0.22.1", require: false
gem "ecdsa"
gem "jwt"
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ GEM
dry-schema (>= 1.12, < 2)
zeitwerk (~> 2.6)
e2mmap (0.1.0)
ecdsa (1.2.0)
erubi (1.11.0)
et-orbi (1.2.7)
tzinfo
Expand Down Expand Up @@ -225,6 +226,7 @@ GEM
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
json (2.6.3)
jwt (2.7.1)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
Expand Down Expand Up @@ -488,6 +490,7 @@ DEPENDENCIES
database_cleaner-active_record
digest-crc
dotenv-rails
ecdsa
factory_bot_rails
faker
fast_jsonapi
Expand All @@ -496,6 +499,7 @@ DEPENDENCIES
hiredis-client
http
jbuilder
jwt
kaminari
kredis
listen (>= 3.0.5)
Expand Down
22 changes: 19 additions & 3 deletions app/controllers/api/v2/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ class BaseController < ActionController::API

rescue_from Api::V2::Exceptions::Error, with: :api_error

protected

def address_to_lock_hash(address)
if address =~ /\A0x/
if address.start_with?("0x")
address
else
parsed = CkbUtils.parse_address(address)
Expand All @@ -24,6 +22,24 @@ def pagy_get_items(collection, pagy)
def api_error(error)
render json: RequestErrorSerializer.new([error], message: error.title), status: error.status
end

attr_reader :current_user

def validate_jwt!
jwt = request.headers["Authorization"]&.split&.last
payload = PortfolioUtils.decode_jwt(jwt)

user = User.find_by(uuid: payload[0]["uuid"])
raise Api::V2::Exceptions::UserNotExistError.new("validate jwt") unless user

@current_user = user
rescue JWT::VerificationError => e
raise Api::V2::Exceptions::DecodeJWTFailedError.new(e.message)
rescue JWT::ExpiredSignature => e
raise Api::V2::Exceptions::DecodeJWTFailedError.new(e.message)
rescue JWT::DecodeError => e
raise Api::V2::Exceptions::DecodeJWTFailedError.new(e.message)
end
end
end
end
18 changes: 18 additions & 0 deletions app/controllers/api/v2/portfolio/addresses_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module Api
module V2
module Portfolio
class AddressesController < BaseController
before_action :validate_jwt!

def create
address_hashes = params.fetch(:addresses, [])
::Portfolio.sync_addresses(current_user, address_hashes)

head :no_content
rescue StandardError => e
raise Api::V2::Exceptions::SyncPortfolioAddressesError
end
end
end
end
end
87 changes: 87 additions & 0 deletions app/controllers/api/v2/portfolio/ckb_transactions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
module Api
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

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
end

def download_csv
args = download_params.merge(address_ids: current_user.address_ids)
file = CsvExportable::ExportPortfolioTransactionsJob.perform_now(args.to_h)

send_data file, type: "text/csv; charset=utf-8; header=present",
disposition: "attachment;filename=portfolio_ckb_transactions.csv"
end

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}")
end

def download_params
params.permit(:start_date, :end_date, :start_number, :end_number)
end
end
end
end
end
32 changes: 32 additions & 0 deletions app/controllers/api/v2/portfolio/sessions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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 }

render json: {
name: user.name,
jwt: PortfolioUtils.generate_jwt(payload)
}
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
end
end
end
end
end
40 changes: 40 additions & 0 deletions app/controllers/api/v2/portfolio/statistics_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module Api
module V2
module Portfolio
class StatisticsController < BaseController
before_action :validate_jwt!, :check_addresses_consistent!

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
end
end
end
end
end
22 changes: 22 additions & 0 deletions app/controllers/api/v2/portfolio/udt_accounts_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module Api
module V2
module Portfolio
class UdtAccountsController < BaseController
before_action :validate_jwt!

def index
expires_in 30.minutes, public: true, stale_while_revalidate: 10.minutes, stale_if_error: 10.minutes

statistic = Portfolios::UdtAccountsStatistic.new(current_user)
if params[:cell_type] == "sudt"
accounts = statistic.sudt_accounts(params[:published])
else
accounts = statistic.nft_accounts
end

render json: accounts
end
end
end
end
end
15 changes: 15 additions & 0 deletions app/controllers/api/v2/portfolio/users_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Api
module V2
module Portfolio
class UsersController < BaseController
before_action :validate_jwt!

def update
current_user.update(name: params[:name])

head :no_content
end
end
end
end
end
Loading

0 comments on commit a8ccc16

Please sign in to comment.