Initial commit
This commit is contained in:
75
app/api/v2/account/balances.rb
Normal file
75
app/api/v2/account/balances.rb
Normal 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
|
||||
205
app/api/v2/account/beneficiaries.rb
Normal file
205
app/api/v2/account/beneficiaries.rb
Normal 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
|
||||
30
app/api/v2/account/bonuses.rb
Normal file
30
app/api/v2/account/bonuses.rb
Normal 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
|
||||
207
app/api/v2/account/deposits.rb
Normal file
207
app/api/v2/account/deposits.rb
Normal 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
|
||||
90
app/api/v2/account/internal_transfers.rb
Normal file
90
app/api/v2/account/internal_transfers.rb
Normal 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
|
||||
28
app/api/v2/account/levels.rb
Normal file
28
app/api/v2/account/levels.rb
Normal 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
|
||||
21
app/api/v2/account/mount.rb
Normal file
21
app/api/v2/account/mount.rb
Normal 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
78
app/api/v2/account/otp.rb
Normal 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
|
||||
36
app/api/v2/account/stats.rb
Normal file
36
app/api/v2/account/stats.rb
Normal 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
|
||||
109
app/api/v2/account/transactions.rb
Normal file
109
app/api/v2/account/transactions.rb
Normal 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
|
||||
43
app/api/v2/account/treasury.rb
Normal file
43
app/api/v2/account/treasury.rb
Normal 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
|
||||
19
app/api/v2/account/utils.rb
Normal file
19
app/api/v2/account/utils.rb
Normal 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
|
||||
245
app/api/v2/account/withdraws.rb
Normal file
245
app/api/v2/account/withdraws.rb
Normal 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
|
||||
Reference in New Issue
Block a user