Initial commit

This commit is contained in:
Yaser
2026-08-13 19:50:53 +03:30
commit 38084458fe
879 changed files with 95198 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Account
class Balances < Grape::API
helpers ::API::V2::ParamHelpers
# TODO: Add failures.
# TODO: Move desc hash options to block once issues are resolved.
# https://github.com/ruby-grape/grape/issues/1789
# https://github.com/ruby-grape/grape-swagger/issues/705
desc 'Get list of user accounts',
is_array: true,
success: API::V2::Entities::Account
params do
use :pagination
optional :nonzero,
type: { value: Boolean, message: 'account.balances.invalid_nonzero' },
default: false,
desc: 'Filter non zero balances.'
optional :search, type: JSON, default: {} do
optional :currency_code,
as: :code,
type: String
optional :currency_name,
as: :name,
type: String
end
end
get '/balances' do
user_authorize! :read, ::Operations::Account
search_params = params[:search]
.slice(:code, :name)
.transform_keys {|k| "#{k}_cont"}
.merge(m: 'or')
accounts = ::Currency.visible.ransack(search_params).result.each_with_object([]) do |c, result|
account = ::Account.find_by(currency: c, member: current_user)
if account.present?
next if params[:nonzero].present? && account.amount.zero? && account.locked.zero?
result << account
elsif account.blank? && params[:nonzero].blank?
result << ::Account.new(currency: c, member: current_user)
end
end
present paginate(accounts),
with: Entities::Account, current_user: current_user
end
desc 'Get user account by currency' do
success API::V2::Entities::Account
# TODO: Add failures.
end
params do
requires :currency,
type: String,
values: { value: -> { Currency.visible.pluck(:id) }, message: 'account.currency.doesnt_exist' },
desc: 'The currency code.'
end
get '/balances/:currency', requirements: { currency: /[\w\.\-]+/ } do
user_authorize! :read, ::Operations::Account
present current_user.accounts.visible.find_by!(currency_id: params[:currency]),
with: API::V2::Entities::Account
end
end
end
end
end

View File

@@ -0,0 +1,205 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Account
class Beneficiaries < Grape::API
before { withdraws_must_be_permitted! }
namespace :beneficiaries do
desc 'Get list of user beneficiaries',
is_array: true,
success: API::V2::Entities::Beneficiary
params do
optional :currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist' },
as: :currency_id,
desc: 'Beneficiary currency code.'
optional :state,
type: String,
values: { value: -> { ::Beneficiary::STATES_AVAILABLE_FOR_MEMBER.map(&:to_s) }, message: 'account.beneficiary.invalid_state'},
desc: 'Defines either beneficiary active - user can use it to withdraw money'\
'or pending - requires beneficiary activation with pin.'
end
get do
user_authorize! :read, ::Beneficiary
current_user
.beneficiaries
.available_to_member
.tap do |q|
q.where!(currency_id: params[:currency_id]) if params[:currency_id].present?
end
.tap do |q|
q.where!(state: params[:state]) if params[:state].present?
end
.yield_self do |b|
present paginate(b), with: API::V2::Entities::Beneficiary
end
end
desc 'Get beneficiary by ID',
success: API::V2::Entities::Beneficiary
params do
requires :id,
type: { value: Integer, message: 'account.beneficiary.non_integer_id' },
desc: 'Beneficiary Identifier in Database'
end
get ':id' do
user_authorize! :read, ::Beneficiary
current_user
.beneficiaries
.available_to_member
.find_by!(id: params[:id])
.yield_self { |b| present b, with: API::V2::Entities::Beneficiary }
end
desc 'Create new beneficiary',
success: API::V2::Entities::Beneficiary
params do
requires :currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist' },
as: :currency_id,
desc: 'Beneficiary currency code.'
requires :name,
type: String,
allow_blank: false,
values: { value: ->(v) { v.present? && v.size <= 64 }, message: 'account.beneficiary.too_long_name' },
desc: 'Human rememberable name which refer beneficiary.'
optional :description,
type: String,
values: { value: ->(v) { v.size <= 255 }, message: 'account.beneficiary.too_long_description' },
desc: 'Human rememberable name which refer beneficiary.'
requires :data,
type: { value: JSON, message: 'account.beneficiary.non_json_data' },
allow_blank: false,
desc: 'Beneficiary data in JSON format'
end
post do
user_authorize! :create, ::Beneficiary
declared_params = declared(params)
currency = Currency.find_by!(id: params[:currency_id])
if !currency.withdrawal_enabled?
error!({ errors: ['account.currency.withdrawal_disabled'] }, 422)
elsif currency.coin? && declared_params.dig(:data, :address).blank?
error!({ errors: ['account.beneficiary.missing_address_in_data'] }, 422)
elsif currency.fiat? && declared_params.dig(:data, :full_name).blank?
error!({ errors: ['account.beneficiary.missing_full_name_in_data'] }, 422)
end
# Since data is stored in MySQL JSON format we iterate through all
# beneficiaries one by one to detect duplicated address.
if currency.coin? &&
current_user
.beneficiaries
.available_to_member
.where(currency: currency)
.any? { |b| b.data['address'] == declared_params.dig(:data, :address) }
error!({ errors: ['account.beneficiary.duplicate_address'] }, 422)
end
present current_user
.beneficiaries
.create!(declared_params),
with: API::V2::Entities::Beneficiary
rescue ActiveRecord::RecordInvalid => e
report_exception(e)
error!({ errors: ['account.beneficiary.failed_to_create'] }, 422)
end
desc 'Resend beneficiary pin'
params do
requires :id,
type: { value: Integer, message: 'account.beneficiary.non_integer_id' },
desc: 'Beneficiary Identifier in Database'
end
patch ':id/resend_pin' do
user_authorize! :update, ::Beneficiary
beneficiary = current_user
.beneficiaries
.available_to_member
.find_by!(id: params[:id])
unless beneficiary.pending?
error!({ errors: ['account.beneficiary.cant_resend'] }, 422)
end
if Time.now - beneficiary.sent_at < 60
error!({ errors: ['account.beneficiary.cant_resend_within_1_minute'], sent_at: beneficiary.sent_at.iso8601 }, 422)
end
beneficiary.regenerate_pin!
status 204
end
desc 'Activates beneficiary with pin',
success: API::V2::Entities::Beneficiary
params do
requires :id,
type: { value: Integer, message: 'account.beneficiary.non_integer_id' },
desc: 'Beneficiary Identifier in Database'
requires :pin,
type: { value: Integer, message: 'account.beneficiary.non_integer_pin' },
desc: 'Pin code for beneficiary activation'
end
patch ':id/activate' do
user_authorize! :update, ::Beneficiary
beneficiary = current_user
.beneficiaries
.available_to_member
.find_by!(id: params[:id])
unless beneficiary.pending?
error!({ errors: ['account.beneficiary.cant_activate'] }, 422)
end
if beneficiary.activate!(params[:pin])
present beneficiary, with: API::V2::Entities::Beneficiary
else
error!({ errors: ['account.beneficiary.invalid_pin'] }, 422)
end
end
desc 'Delete beneficiary'
params do
requires :id,
type: { value: Integer, message: 'account.beneficiary.non_integer_id' },
desc: 'Beneficiary Identifier in Database'
end
delete ':id' do
user_authorize! :destroy, ::Beneficiary
beneficiary = current_user
.beneficiaries
.available_to_member
.find_by!(id: params[:id])
if beneficiary.archive!
body false
else
error!({ errors: ['account.beneficiary.cant_delete'] }, 422)
end
end
end
end
end
end
end

View File

@@ -0,0 +1,30 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Account
class Bonuses < Grape::API
desc 'Get bonus simple data of a user',
is_array: true
get '/bonus' do
user_bonuses = Bonus.where(bonus_member: current_user)
all = user_bonuses.inject(0){ |sum, x| sum + x.amount }
h24 = user_bonuses.h24.inject(0){ |sum, x| sum + x.amount }
number = Member.where(referral_uid: current_user.uid).count
{ 'h24': h24, 'all': all, 'number': number }
end
desc 'Get bonus report',
is_array: true
get '/bonus/report' do
bonus = Bonus.where(bonus_member: current_user)
present paginate(bonus), with: Entities::Bonus
end
end
end
end
end

View File

@@ -0,0 +1,207 @@
# encoding: UTF-8
# frozen_string_literal: true
require_relative '../validations'
module API
module V2
module Account
class Deposits < Grape::API
before { deposits_must_be_permitted! }
desc 'Get your deposits history.',
is_array: true,
success: API::V2::Entities::Deposit
params do
optional :currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist' },
desc: 'Currency code'
optional :state,
values: { value: ->(v) { (Array.wrap(v) - ::Deposit.aasm.states.map(&:name).map(&:to_s)).blank? }, message: 'account.deposit.invalid_state' },
desc: 'Filter deposits by states.'
optional :txid,
type: String,
allow_blank: false,
desc: 'Deposit transaction id.'
optional :time_from,
allow_blank: { value: false, message: 'account.deposit.empty_time_from' },
type: { value: Integer, message: 'account.deposit.non_integer_time_from' },
desc: 'An integer represents the seconds elapsed since Unix epoch.'
optional :time_to,
type: { value: Integer, message: 'account.deposit.non_integer_time_to' },
allow_blank: { value: false, message: 'account.deposit.empty_time_to' },
desc: 'An integer represents the seconds elapsed since Unix epoch.'
optional :limit,
type: { value: Integer, message: 'account.deposit.non_integer_limit' },
values: { value: 1..100, message: 'account.deposit.invalid_limit' },
default: 100,
desc: "Number of deposits per page (defaults to 100, maximum is 100)."
optional :page,
type: { value: Integer, message: 'account.deposit.non_integer_page' },
values: { value: -> (p){ p.try(:positive?) }, message: 'account.deposit.non_positive_page'},
default: 1,
desc: 'Page number (defaults to 1).'
end
get "/deposits" do
user_authorize! :read, ::Deposit
currency = Currency.find(params[:currency]) if params[:currency].present?
current_user.deposits.order(id: :desc)
.tap { |q| q.where!(currency: currency) if currency }
.tap { |q| q.where!(txid: params[:txid]) if params[:txid] }
.tap { |q| q.where!(aasm_state: params[:state]) if params[:state] }
.tap { |q| q.where!('updated_at >= ?', Time.at(params[:time_from])) if params[:time_from].present? }
.tap { |q| q.where!('updated_at <= ?', Time.at(params[:time_to])) if params[:time_to].present? }
.tap { |q| present paginate(q), with: API::V2::Entities::Deposit }
end
desc 'Get details of specific deposit.' do
success API::V2::Entities::Deposit
end
params do
requires :txid,
type: String,
allow_blank: false,
desc: "Deposit transaction id"
end
get "/deposits/:txid" do
user_authorize! :read, ::Deposit
deposit = current_user.deposits.find_by!(txid: params[:txid])
present deposit, with: API::V2::Entities::Deposit
end
desc 'Returns deposit address for account you want to deposit to by currency. ' \
'The address may be blank because address generation process is still in progress. ' \
'If this case you should try again later.',
success: API::V2::Entities::Deposit
params do
requires :currency,
type: String,
values: { value: -> { Currency.coins.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist'},
desc: 'The account you want to deposit to.'
given :currency do
optional :address_format,
type: String,
values: { value: -> { %w[legacy cash] }, message: 'account.deposit_address.invalid_address_format' },
validate_currency_address_format: { value: true, prefix: 'account.deposit_address' },
desc: 'Address format legacy/cash'
end
end
get '/deposit_address/:currency', requirements: { currency: /[\w\.\-]+/ } do
user_authorize! :read, ::PaymentAddress
currency = Currency.find(params[:currency])
unless currency.deposit_enabled?
error!({ errors: ['account.currency.deposit_disabled'] }, 422)
end
wallet = Wallet.deposit_wallet(currency.id)
unless wallet.present?
error!({ errors: ['account.wallet.not_found'] }, 422)
end
payment_address = current_user.payment_address(wallet.id)
present payment_address, with: API::V2::Entities::PaymentAddress, address_format: params[:address_format]
end
############
desc 'Create fiat deposits'
params do
requires :amount,
type: { value: BigDecimal, message: 'deposit.fiat.non_decimal_amount' },
values: { value: ->(v) { v.try(:positive?) }, message: 'deposit.fiat.non_positive_amount' },
desc: 'The amount to deposit fiat.'
requires :callback_url,
regexp: { value: URI::regexp, message: 'callback_url.invalid' },
desc: -> { 'callback_url payment url' }
requires :currency,
type: String,
default: 'irt',
desc: -> { 'currency' }
requires :card,
type: String,
desc: 'card bank number'
optional :factorNumber,
type: String,
allow_blank: true,
desc: 'factor number'
optional :description,
type: String,
allow_blank: true,
desc: 'description'
end
post '/deposits/fiat' do
user_authorize! :create, ::Deposit
error!({ errors: ['account.deposit.use_valid_card'] }, 403) if current_user.cards.exclude?(params[:card].to_s)
deposit_data = { member: current_user, currency: Currency.find(params['currency']), aasm_state: 'canceled',
address: params['card'], amount: params['amount'], transfer_type: :fiat }
deposit = ::Deposits::Fiat.new(deposit_data)
error!({ errors: deposit.errors.full_messages }, 422) if deposit.errors.any?
data = { amount: (params[:amount] * 10), callback_url: params[:callback_url],
description: params[:description], valid_card_number: params[:card] }
response = VandarService::generate_token(data)
if response.dig('status').to_i.positive?
deposit.tap { |d| d.tid = response.dig('token') }.save!
present(response: response.dig('token'))
else
present(response: response)
status 422
end
end
desc 'Confirm fiat deposits'
params do
requires :token,
type: String,
allow_blank: false,
desc: 'payment token'
end
post '/deposits/confirm' do
deposit = ::Deposits::Fiat.find_by(tid: params[:token])
error!({ errors: ['corresponding.deposit.record.not.found'] }, 403) unless deposit.present?
# because vandar just response one
if deposit.txid.present?
db_response = {
status: 1,
amount: deposit.amount,
wage: deposit.fee,
cardNumber: deposit.address,
transId: deposit.txid
}
return present(response: db_response)
end
# first step: check everything is ok
response = ::VandarService.transaction(params[:token])
if response.dig('status').to_i.positive? && response.dig('trackingCode').present?
if response['cardNumber'].last(4).to_i == deposit.address.last(4).to_i
response = VandarService.verify(params[:token])
if response.dig('status').to_i.positive?
deposit.update(txid: response['transId'], fee: response['wage'].to_f / 10, aasm_state: 'submitted')
deposit.charge!
end
end
# TODO we should see the response and check it for rial and toman confirmation
present(response: response)
else
present(response: response)
status 422
end
# second step: verify payment
end
end
end
end
end

View File

@@ -0,0 +1,90 @@
# frozen_string_literal: true
module API
module V2
module Account
class InternalTransfers < Grape::API
namespace :internal_transfers do
desc 'List your internal transfers as paginated collection.',
is_array: true,
success: API::V2::Entities::InternalTransfer
params do
optional :currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist' },
desc: 'Currency code.'
optional :state, type: String, desc: 'The state to filter by.'
optional :sender
end
get do
user_authorize! :read, ::InternalTransfer
ransack_params = ::API::V2::Admin::Helpers::RansackBuilder.new(params)
.eq(:state)
.translate(currency: :currency_id)
.merge(g: [
{ sender_id_eq: current_user.id, receiver_id_eq: current_user.id, m: 'or' }
]).build
search = InternalTransfer.ransack(ransack_params)
.result
.order('id desc')
present paginate(search), with: API::V2::Entities::InternalTransfer, current_user: current_user
end
desc 'Creates internal transfer.'
params do
requires :currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist' },
desc: 'The currency code.'
requires :amount,
type: { value: BigDecimal, message: 'account.internal_transfer.non_decimal_amount' },
values: { value: ->(v) { v.try(:positive?) }, message: 'account.internal_transfer.non_positive_amount' },
desc: 'The amount to transfer.'
requires :otp,
type: { value: Integer, message: 'account.internal_transfer.non_integer_otp' },
allow_blank: false,
desc: 'OTP to perform action'
requires :username_or_uid,
type: String,
allow_blank: false,
desc: 'Receiver uid or username.'
end
post do
receiver = Member.find_by_username_or_uid(params[:username_or_uid])
error!({ errors: ['account.internal_transfer.receiver_not_found'] }, 422) if receiver.nil?
currency = Currency.find(params[:currency])
unless Vault::TOTP.validate?(current_user.uid, params[:otp])
error!({ errors: ['account.internal_transfer.invalid_otp'] }, 422)
end
if current_user.get_account(currency).balance < params[:amount]
error!({ errors: ['account.internal_transfer.insufficient_balance'] }, 422)
end
if current_user == receiver
error!({ errors: ['account.internal_transfer.can_not_tranfer_to_yourself'] }, 422)
end
internal_transfer = ::InternalTransfer.new(
currency: currency,
sender: current_user,
receiver: receiver,
amount: params[:amount]
)
if internal_transfer.save
present internal_transfer, with: API::V2::Entities::InternalTransfer
status 201
else
body errors: internal_transfer.errors.full_messages
status 422
end
end
end
end
end
end
end

View File

@@ -0,0 +1,28 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Account
class Levels < Grape::API
desc 'Get current level and next level informations',
is_array: true
get '/levels' do
current_level = current_user.group
next_level = current_user.next_group
current_level_min_max = Member.min_max_group(current_level)
next_level_min_max = Member.min_max_group(next_level)
current_level_fee = TradingFee.for(group: current_level, market_id: 'ANY')
next_level_fee = TradingFee.for(group: next_level, market_id: 'ANY')
{ 'current': { 'name': current_level, 'max': current_level_min_max[1], 'min': current_level_min_max[0],
'maker_fee': current_level_fee.maker, 'taker_fee': current_level_fee.taker },
'next': { 'name': next_level, 'max': next_level_min_max[1], 'min': next_level_min_max[0],
'maker_fee': next_level_fee.maker, 'taker_fee': next_level_fee.taker }}
end
end
end
end
end

View File

@@ -0,0 +1,21 @@
module API::V2
module Account
class Mount < Grape::API
before { authenticate! }
before { set_ets_context! }
mount Account::Balances
mount Account::Deposits
mount Account::Beneficiaries
mount Account::Withdraws
mount Account::Transactions
mount Account::Stats
mount Account::InternalTransfers
mount Account::Otp
mount Account::Treasury
mount Account::Levels
mount Account::Bonuses
end
end
end

78
app/api/v2/account/otp.rb Normal file
View File

@@ -0,0 +1,78 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Account
class Otp < Grape::API
helpers API::V2::Account::Utils
resource :otp do
# for example
# for withdraw with id 3
# we create a cache with name -> withdraw_3_2fa
#
# # for example
# # for deposit with id 6
# # we create a cache with name -> deposit_6_2fa
desc 'validate user otp'
params do
requires :otp,
type: { value: Integer, message: 'integer_otp' },
allow_blank: false,
desc: 'OTP to perform action'
optional :action,
type: String,
desc: 'action for top'
# optional :id,
# type: String,
# desc: 'if for top action'
end
post do
error!({ errors: ['account.not_active_otp'] }, 422) unless current_user.otp
error!({ errors: ['account.invalid_otp'] }, 422) unless Vault::TOTP.validate?(current_user.uid, params[:otp])
action = 'validate'
# id = '0'
action = params[:action] if params[:action].present?
# id = params[:id] if params[:id].present?
current_user.write_cache("#{action}_2fa", 1, 60)
status 200
end
desc 'Send confirmations code',
success: { code: 201, message: 'Generated verification code' },
failure: [
{ code: 400, message: 'Required params are missing' },
{ code: 422, message: 'Validation errors' }
]
params do
requires :data,
type: String,
allow_blank: false,
desc: 'Account email or Telephone number'
requires :action,
type: String,
allow_blank: false,
desc: 'for what need auth code'
optional :channel,
type: String,
allow_blank: true,
default: 'email',
desc: 'channel that send in'
optional :captcha_response,
types: [String, Hash],
desc: 'Response from captcha widget'
end
post '/resend' do
current_user = Member.find_by(email: params[:data])
return status 201 if current_user.nil?
publish_confirmation_code(current_user, params[:action])
status 201
end
end
end
end
end
end

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
module API
module V2
module Account
class Stats < Grape::API
desc 'Get assets pnl calculated into one currency'
params do
optional :pnl_currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'pnl.currency.doesnt_exist' },
desc: 'Currency code in which the PnL is calculated'
end
get '/stats/pnl' do
user_authorize! :read, ::StatsMemberPnl
query = 'SELECT pnl_currency_id, currency_id, total_credit, total_debit, total_credit_value, total_debit_value, ' \
'total_credit_value / NULLIF(total_credit, 0) "average_buy_price", ' \
'total_debit_value / NULLIF(total_debit, 0) "average_sell_price", ' \
'average_balance_price, total_balance_value ' \
'FROM stats_member_pnl WHERE member_id = ?'
conditions = [current_user.id]
if params[:pnl_currency].present?
query += ' AND pnl_currency_id = ?'
conditions << params[:pnl_currency]
end
squery = ActiveRecord::Base.sanitize_sql_for_conditions([query] + conditions)
result = ActiveRecord::Base.connection.exec_query(squery).to_hash
present result.each(&:symbolize_keys!), with: API::V2::Entities::Pnl
end
end
end
end
end

View File

@@ -0,0 +1,109 @@
# encoding: UTF-8
# frozen_string_literal: true
require_relative '../validations'
module API
module V2
module Account
class Transactions < Grape::API
before { deposits_must_be_permitted! }
before { withdraws_must_be_permitted! }
desc 'Get your transactions history.',
is_array: true
params do
optional :currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.transactions.currency_doesnt_exist' },
desc: 'Currency code'
optional :order_by,
type: String,
values: { value: %w(asc desc), message: 'account.transactions.order_by_invalid' },
default: 'desc',
desc: 'Sorting order'
optional :time_from,
allow_blank: { value: false, message: 'account.transactions.empty_time_from' },
type: { value: Integer, message: 'account.transactions.non_integer_time_from' },
desc: 'An integer represents the seconds elapsed since Unix epoch.'
optional :time_to,
type: { value: Integer, message: 'account.transactions.non_integer_time_to' },
allow_blank: { value: false, message: 'account.transactions.empty_time_to' },
desc: 'An integer represents the seconds elapsed since Unix epoch.'
optional :deposit_state,
values: { value: ->(v) { (Array.wrap(v) - ::Deposit.aasm.states.map(&:name).map(&:to_s)).blank? }, message: 'account.transactions.invalid_deposit_state' },
desc: 'Filter deposits by states.',
default: []
optional :withdraw_state,
values: { value: ->(v) { (Array.wrap(v) - Withdraw::STATES.map(&:to_s)).blank? }, message: 'account.transactions.invalid_withdraw_state' },
desc: 'Filter withdraws by states.',
default: []
optional :txid,
type: String,
allow_blank: false,
desc: 'Transaction id.'
optional :limit,
type: { value: Integer, message: 'account.transactions.non_integer_limit' },
values: { value: 1..1000, message: 'account.transactions.invalid_limit' },
default: 100,
desc: 'Limit the number of returned transactions. Default to 100.'
optional :page,
type: { value: Integer, message: 'account.transactions.non_integer_page' },
values: { value: -> (p){ p.try(:positive?) }, message: 'account.transactions.non_positive_page'},
allow_blank: false,
default: 1,
desc: 'Specify the page of paginated results.'
end
get "/transactions" do
user_authorize! :read, ::Withdraw
user_authorize! :read, ::Deposit
deposit_state = params[:deposit_state]&.split(/\W+/)&.join(',')
withdraw_state = params[:withdraw_state]&.split(/\W+/)&.join(',')
deposit_sql = "(SELECT d.id, currency_id, amount, fee, address, aasm_state, NULL AS note, txid, d.created_at, d.updated_at, d.type, b.height - block_number AS confirmations FROM deposits d " \
"INNER JOIN currencies c ON c.id=d.currency_id LEFT JOIN blockchains b ON b.key=c.blockchain_key WHERE member_id=#{current_user.id} "
if params[:deposit_state].present?
deposit_sql += if Rails.configuration.database_adapter.downcase == 'PostgreSQL'.downcase
"and aasm_state = any(string_to_array('#{deposit_state}',','))"
else
"and FIND_IN_SET(aasm_state, '#{deposit_state}')"
end
end
withdraw_sql = "SELECT w.id, currency_id, amount, fee, rid, aasm_state, note, txid, w.created_at, w.updated_at, w.type, b.height - block_number AS confirmations FROM withdraws w " \
"INNER JOIN currencies c ON c.id=w.currency_id LEFT JOIN blockchains b ON b.key=c.blockchain_key WHERE member_id=#{current_user.id} "
if params[:withdraw_state].present?
withdraw_sql += if Rails.configuration.database_adapter.downcase == 'PostgreSQL'.downcase
"and aasm_state = any(string_to_array('#{withdraw_state}',','))"
else
"and FIND_IN_SET(aasm_state, '#{withdraw_state}')"
end
end
sql = "SELECT * FROM " + deposit_sql + "UNION " + withdraw_sql + ") AS transactions ORDER BY updated_at #{params[:order_by].upcase}"
result = ActiveRecord::Base.connection.exec_query(sql).to_hash
result.select! { |t| t['currency_id'] == params[:currency].downcase } if params[:currency].present?
result.select! { |t| t['txid'] == params[:txid] } if params[:txid].present?
result.select! { |t| t['updated_at'] >= Time.at(params[:time_from]) } if params[:time_from].present?
result.select! { |t| t['updated_at'] <= Time.at(params[:time_to]) } if params[:time_to].present?
present paginate(result.each(&:symbolize_keys!)), with: API::V2::Entities::Transactions
end
end
end
end
end

View File

@@ -0,0 +1,43 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Account
class Treasury < Grape::API
helpers ::API::V2::ParamHelpers
desc 'Get list of user cards',
is_array: true
params do
# use :pagination
optional :uid,
type: String,
default: false,
desc: 'UID of user'
end
get '/treasury/cards' do
user_authorize! :read, ::Operations::Account
present current_user.cards
end
desc 'Get list of user ibans',
is_array: true
params do
# use :pagination
optional :uid,
type: String,
default: false,
desc: 'UID of user'
end
get '/treasury/ibans' do
user_authorize! :read, ::Operations::Account
present current_user.ibans
end
end
end
end
end

View File

@@ -0,0 +1,19 @@
module API
module V2
module Account
module Utils
def session
request.session
end
def publish_confirmation_code(user, action)
totp = ::Vault::TOTPAction.new(action)
totp.create(user.uid, user.email)
::EventAPI.notify(action, record: { user: user.as_json_for_event_api,
domain: Peatio::App.config.domain,
code: totp.read_code(user.uid) })
end
end
end
end
end

View File

@@ -0,0 +1,245 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Account
class Withdraws < Grape::API
before { withdraws_must_be_permitted! }
helpers API::V2::Account::Utils
desc 'List your withdraws as paginated collection.',
is_array: true,
success: API::V2::Entities::Withdraw
params do
optional :currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist'},
desc: 'Currency code.'
optional :limit,
type: { value: Integer, message: 'account.withdraw.non_integer_limit' },
values: { value: 1..100, message: 'account.withdraw.invalid_limit' },
default: 100,
desc: "Number of withdraws per page (defaults to 100, maximum is 100)."
optional :state,
values: { value: ->(v) { (Array.wrap(v) - Withdraw::STATES.map(&:to_s)).blank? }, message: 'account.withdraw.invalid_state' },
desc: 'Filter withdrawals by states.'
optional :rid,
type: String,
allow_blank: false,
desc: 'Wallet address on the Blockchain.'
optional :time_from,
allow_blank: { value: false, message: 'account.withdraw.empty_time_from' },
type: { value: Integer, message: 'account.withdraw.non_integer_time_from' },
desc: 'An integer represents the seconds elapsed since Unix epoch.'
optional :time_to,
type: { value: Integer, message: 'account.withdraw.non_integer_time_to' },
allow_blank: { value: false, message: 'account.withdraw.empty_time_to' },
desc: 'An integer represents the seconds elapsed since Unix epoch.'
optional :page,
type: { value: Integer, message: 'account.withdraw.non_integer_page' },
values: { value: -> (p){ p.try(:positive?) }, message: 'account.withdraw.non_positive_page'},
default: 1,
desc: 'Page number (defaults to 1).'
end
get '/withdraws' do
user_authorize! :read, ::Withdraw
currency = Currency.find(params[:currency]) if params[:currency].present?
current_user.withdraws.order(id: :desc)
.tap { |q| q.where!(currency: currency) if currency }
.tap { |q| q.where!(aasm_state: params[:state]) if params[:state] }
.tap { |q| q.where!(rid: params[:rid]) if params[:rid] }
.tap { |q| q.where!('updated_at >= ?', Time.at(params[:time_from])) if params[:time_from].present? }
.tap { |q| q.where!('updated_at <= ?', Time.at(params[:time_to])) if params[:time_to].present? }
.tap { |q| present paginate(q), with: API::V2::Entities::Withdraw }
end
desc 'Returns withdrawal sums for last 4 hours and 1 month'
get '/withdraws/sums' do
user_authorize! :read, ::Withdraw
sum_24_hours, sum_1_month = Withdraw.sanitize_execute_sum_queries(current_user.id)
present({ last_24_hours: sum_24_hours, last_1_month: sum_1_month })
end
desc 'Creates new withdrawal to active beneficiary.'
params do
requires :otp,
type: { value: Integer, message: 'account.withdraw.non_integer_otp' },
allow_blank: false,
desc: 'OTP to perform action'
requires :beneficiary_id,
type: { value: Integer, message: 'account.withdraw.non_integer_beneficiary_id' },
allow_blank: false,
desc: 'ID of Active Beneficiary belonging to user.'
requires :currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist'},
desc: 'The currency code.'
requires :amount,
type: { value: BigDecimal, message: 'account.withdraw.non_decimal_amount' },
values: { value: ->(v) { v.try(:positive?) }, message: 'account.withdraw.non_positive_amount' },
desc: 'The amount to withdraw.'
optional :note,
type: String,
values: { value: ->(v) { v.size <= 256 }, message: 'account.withdraw.too_long_note' },
desc: 'Optional user metadata to be applied to the transaction. Used to tag transactions with memorable comments.'
end
post '/withdraws' do
user_authorize! :create, ::Withdraw
withdraw_api_must_be_enabled!
if current_user.otp.present? && current_user.read_cache('withdraw_2fa').blank?
error!({ errors: ['account.withdraw.need.2fa'] }, 403)
end
totp = Vault::TOTPAction.new('withdraw-coin')
error!({ errors: ['withdraw.totp.code'] }, 422) unless totp.validate?(current_user.uid, params[:otp])
beneficiary = current_user
.beneficiaries
.available_to_member
.find_by(id: params[:beneficiary_id])
if beneficiary.blank?
error!({ errors: ['account.beneficiary.doesnt_exist'] }, 422)
elsif !beneficiary.active?
error!({ errors: ['account.beneficiary.invalid_state_for_withdrawal'] }, 422)
end
currency = Currency.find(params[:currency])
error!({ errors: ['account.currency.withdrawal_disabled'] }, 422) unless currency.withdrawal_enabled?
# TODO: Delete subclasses from Deposit and Withdraw
withdraw = "withdraws/#{currency.type}".camelize.constantize.new \
beneficiary: beneficiary,
sum: params[:amount],
member: current_user,
currency: currency,
note: params[:note]
withdraw.save!
withdraw.with_lock { withdraw.accept! }
current_user.delete_cache('withdraw_2fa')
present withdraw, with: API::V2::Entities::Withdraw
rescue ::Account::AccountError => e
report_api_error(e, request)
error!({ errors: ['account.withdraw.insufficient_balance'] }, 422)
rescue ActiveRecord::RecordInvalid => e
report_api_error(e, request)
# TODO: Check if there are other errors possible here.
# For now single error which is not handled by params validations is
# sum precision validation error (PrecisionValidator).
error!({ errors: ['account.withdraw.invalid_amount'] }, 422)
rescue => e
report_exception(e)
error!({ errors: ['account.withdraw.create_error'] }, 422)
end
desc 'Creates new fiat withdrawal'
params do
requires :amount,
type: { value: BigDecimal, message: 'account.withdraw.non_decimal_amount' },
values: { value: ->(v) { v.try(:positive?) }, message: 'account.withdraw.non_positive_amount' },
desc: 'The amount to withdraw.'
requires :iban,
type: String,
desc: 'iban bank number'
requires :currency,
type: String,
default: 'irt',
desc: -> { 'currency' }
optional :note,
type: String,
values: { value: ->(v) { v.size <= 256 }, message: 'account.withdraw.too_long_note' },
desc: 'Optional user metadata to be applied to the transaction. Used to tag transactions with memorable comments.'
end
post '/withdraws/fiat' do
user_authorize! :create, ::Withdraw
withdraw_api_must_be_enabled!
if current_user.otp.present? && current_user.read_cache('withdraw_2fa').blank?
error!({ errors: ['account.withdraw.need.2fa'] }, 403)
end
error!({ errors: ['account.withdraw.use_valid_iban'] }, 403) if current_user.ibans.exclude?(params[:iban])
currency = Currency.find(params[:currency])
withdraw = "withdraws/#{currency.type}".camelize.constantize.new \
rid: params[:iban],
sum: params[:amount],
member: current_user,
currency: currency,
transfer_type: :fiat,
note: params[:note]
withdraw.save
error!({ errors: withdraw.errors.full_messages }, 422) unless withdraw.valid?
# delete 2fa cache
current_user.delete_cache('withdraw_2fa')
publish_confirmation_code(current_user, 'withdraw-fiat')
present withdraw, with: API::V2::Entities::Withdraw
end
#
#
desc 'confirm fiat withdrawal'
params do
requires :id,
type: Integer,
desc: 'withdraw id'
requires :otp,
type: Integer,
desc: 'auth otp in sms or email'
end
post '/withdraws/confirm' do
user_authorize! :create, ::Withdraw
withdraw = current_user.withdraws.find_by(id: params[:id])
error!({ errors: ['account.withdraw.not.found'] }, 403) unless withdraw.present?
error!({ errors: ['account.withdraw.process_before'] }, 403) unless withdraw.aasm_state == 'prepared'
totp = Vault::TOTPAction.new('withdraw-fiat')
error!({ errors: ['withdraw.totp.code'] }, 422) unless totp.validate?(current_user.uid, params[:otp])
# here must lock found
withdraw.with_lock { withdraw.accept! }
response = ::VandarService.new.create_withdraw(amount: withdraw.sum, iban: withdraw.rid)
if response.dig('status').to_i.positive?
withdraw.update(txid: response['data'].dig('settlement', 0, 'transaction_id'), aasm_state: 'confirming')
else
withdraw.with_lock { withdraw.reject! }
end
present(response: response)
end
#
#
desc 'Get fiat withdrawal'
params do
requires :id,
type: Integer,
desc: 'withdraw id'
optional :with_otp,
type: String,
desc: -> { 'send otp or not' }
end
get '/withdraw' do
user_authorize! :read, ::Withdraw
withdraw = current_user.withdraws.find_by(id: params[:id])
if withdraw.present? && withdraw.accepted? && params['with_otp'].present?
publish_confirmation_code(current_user, 'withdraw')
end
present withdraw, with: API::V2::Entities::Withdraw
end
end
end
end
end