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

9
app/api/mount.rb Normal file
View File

@@ -0,0 +1,9 @@
module API
class Mount < Grape::API
PREFIX = '/api'
cascade false
mount API::V2::Mount => API::V2::Mount::API_VERSION
end
end

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

View File

@@ -0,0 +1,17 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
class Abilities < Grape::API
namespace :abilities do
desc 'Get all roles and permissions.'
get do
Ability.admin_permissions[current_user.role] || {}
end
end
end
end
end
end

View File

@@ -0,0 +1,155 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
class Adjustments < Grape::API
helpers ::API::V2::Admin::Helpers
namespace :adjustments do
desc 'Get all adjustments, result is paginated.',
is_array: true,
success: API::V2::Admin::Entities::Adjustment
params do
use :currency
use :date_picker
use :pagination
use :ordering
optional :state,
type: String,
values: { value: -> { Adjustment.aasm.states.map(&:name).map(&:to_s) }, message: 'admin.adjustment.invalid_action' },
desc: -> { API::V2::Admin::Entities::Adjustment.documentation[:state][:desc] }
optional :category,
type: String,
values: { value: -> { ::Adjustment::CATEGORIES }, message: 'admin.adjustment.invalid_category' },
desc: -> { API::V2::Admin::Entities::Adjustment.documentation[:category][:desc] }
end
get do
admin_authorize! :read, Adjustment
ransack_params = Helpers::RansackBuilder.new(params)
.eq(:state, :category)
.translate(currency: :currency_id)
.with_daterange
.build
search = Adjustment.ransack(ransack_params)
search.sorts = "#{params[:order_by]} #{params[:ordering]}"
present paginate(search.result), with: API::V2::Admin::Entities::Adjustment
end
desc 'Get adjustment by ID',
success: API::V2::Admin::Entities::Adjustment
params do
requires :id,
type: { value: Integer, message: 'account.adjustment.non_integer_id' },
desc: 'Adjsustment Identifier in Database'
end
get ':id' do
admin_authorize! :read, Adjustment
present ::Adjustment.find(params[:id]), with: API::V2::Admin::Entities::Adjustment
end
desc 'Create new adjustment.',
success: API::V2::Admin::Entities::Adjustment
params do
requires :reason,
type: String,
desc: -> { API::V2::Admin::Entities::Adjustment.documentation[:reason][:desc] }
requires :description,
type: String,
desc: -> { API::V2::Admin::Entities::Adjustment.documentation[:description][:desc] }
requires :category,
type: String,
values: { value: -> { ::Adjustment::CATEGORIES }, message: 'admin.adjustment.invalid_category' },
desc: -> { API::V2::Admin::Entities::Adjustment.documentation[:category][:desc] }
requires :amount,
type: { value: BigDecimal, message: 'admin.adjustment.non_decimal_amount' },
desc: -> { API::V2::Admin::Entities::Adjustment.documentation[:amount][:desc] }
requires :currency_id,
type: String,
values: { value: -> { ::Currency.codes }, message: 'admin.adjustment.currency_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Adjustment.documentation[:currency][:desc] }
requires :asset_account_code,
type: { value: Integer, message: 'admin.adjustment.non_integer_asset_account_code' },
values: { value: -> { ::Operations::Account.where(type: :asset).pluck(:code) }, message: 'admin.adjustment.invalid_asset_account_code' },
desc: -> { API::V2::Admin::Entities::Adjustment.documentation[:asset_account_code][:desc] }
requires :receiving_account_code,
type: { value: Integer, message: 'admin.adjustment.non_integer_receiving_account_code' },
values: { value: -> { ::Operations::Account.where.not(type: :asset).pluck(:code) }, message: 'admin.adjustment.invalid_receiving_account_code' },
desc: -> { API::V2::Admin::Entities::Adjustment.documentation[:receiving_account_code][:desc] }
optional :receiving_member_uid,
type: String,
desc: -> { API::V2::Admin::Entities::Adjustment.documentation[:receiving_account_code][:desc] }
end
post '/new' do
admin_authorize! :create, Adjustment
# Do not accept member_uid if account code is not Liability or Revenue
# Raise error if there is no :receiving_member_uid for Liability
operation_klass = ::Operations.klass_for(code: params[:receiving_account_code])
if operation_klass == ::Operations::Liability && params[:receiving_member_uid].blank?
error!({ errors: ['admin.adjustment.missing_receiving_member_uid'] }, 422)
elsif operation_klass == ::Operations::Expense && params[:receiving_member_uid].present?
error!({ errors: ['admin.adjustment.redundant_receiving_member_uid'] }, 422)
end
receiving = ::Operations.build_account_number(currency_id: params[:currency_id],
account_code: params[:receiving_account_code],
member_uid: params[:receiving_member_uid])
adjustment = Adjustment.new(declared(params)
.except(:receiving_account_code, :receiving_member_uid)
.merge(receiving_account_number: receiving,
creator: current_user))
if adjustment.save
present adjustment, with: API::V2::Admin::Entities::Adjustment
status 201
else
body errors: adjustment.errors.full_messages
status 422
end
end
desc 'Accepts adjustment and creates operations or reject adjustment.',
success: API::V2::Admin::Entities::Adjustment
params do
requires :id,
type: { value: Integer, message: 'admin.adjustment.non_integer_id' },
desc: -> { API::V2::Admin::Entities::Adjustment.documentation[:id][:desc] }
requires :action,
type: String,
values: { value: -> { Adjustment.aasm.events.map(&:name).map(&:to_s) }, message: 'admin.adjustment.invalid_action' },
desc: "Adjustment action all available actions: #{Adjustment.aasm.events.map(&:name)}"
end
post '/action' do
admin_authorize! :update, Adjustment
adjustment = Adjustment.find(params[:id])
if adjustment.amount < 0
account_number_hash = ::Operations.split_account_number(account_number: adjustment.receiving_account_number)
member = Member.find_by(uid: account_number_hash[:member_uid])
balance = member.get_account(account_number_hash[:currency_id]).balance
if adjustment.amount.abs() > balance
error!({ errors: ['admin.adjustment.user_insufficient_balance'] }, 422)
end
end
if adjustment.public_send("may_#{params[:action]}?")
# TODO: Add behaviour in case of errors on action.
adjustment.public_send("#{params[:action]}!", validator: current_user)
present adjustment, with: API::V2::Admin::Entities::Adjustment
else
body errors: ["admin.adjustment.cannot_perform_#{params[:action]}_action"]
status 422
end
end
end
end
end
end
end

View File

@@ -0,0 +1,23 @@
# encoding: UTF-8
# frozen_string_literal: true
require 'peatio/airdrop'
module API
module V2
module Admin
class Airdrops < Grape::API
desc 'Process user airdrop'
params do
requires :file,
type: File
end
post '/airdrops' do
Peatio::Airdrop.new.process(current_user, params)
present(result: 'Airdrop processing started')
end
end
end
end
end

View File

@@ -0,0 +1,77 @@
# frozen_string_literal: true
module API
module V2
module Admin
class Beneficiaries < Grape::API
helpers ::API::V2::Admin::Helpers
namespace :beneficiaries do
desc 'Get list of beneficiaries',
success: API::V2::Admin::Entities::Beneficiary
params do
use :uid
use :ordering
use :pagination
optional :id,
type: Integer,
desc: -> { API::V2::Entities::Beneficiary.documentation[:id][:desc] }
optional :currency,
values: { value: ->(v) { (Array.wrap(v) - ::Currency.codes).blank? }, message: 'account.currency.doesnt_exist' },
desc: 'Beneficiary currency code'
optional :state,
type: Array[Integer],
values: { value: ->(v) { (Array.wrap(v) - ::Beneficiary::STATES_MAPPING.values).blank? }, message: 'account.beneficiary.invalid_state' },
desc: 'Beneficiary state',
coerce_with: lambda { |val|
val.map { |s| Beneficiary::STATES_MAPPING[s.to_sym] }
}
end
get do
admin_authorize! :read, Beneficiary
ransack_params = Helpers::RansackBuilder.new(params)
.eq(:id)
.in(:state)
.translate_in(currency: :currency_id)
.translate(uid: :member_uid)
.build
search = Beneficiary.ransack(ransack_params)
search.sorts = "#{params[:order_by]} #{params[:ordering]}"
present paginate(search.result), with: API::V2::Admin::Entities::Beneficiary
end
desc 'Take an action on the beneficiary',
success: API::V2::Admin::Entities::Beneficiary
params do
requires :id,
type: Integer,
desc: -> { API::V2::Admin::Entities::Beneficiary.documentation[:id][:desc] }
requires :action,
type: String,
values: { value: -> { ::Beneficiary.aasm.events.map(&:name).map(&:to_s) }, message: 'admin.beneficiary.invalid_action' },
desc: "Valid actions are #{::Beneficiary.aasm.events.map(&:name)}."
end
post '/actions' do
admin_authorize! :update, Beneficiary
beneficiary = Beneficiary.find(params[:id])
if beneficiary.public_send("may_#{params[:action]}?")
beneficiary.public_send("#{params[:action]}!")
present beneficiary, with: API::V2::Admin::Entities::Beneficiary
else
body errors: ["admin.beneficiary.cannot_#{params[:action]}"]
status 422
end
end
end
end
end
end
end

View File

@@ -0,0 +1,201 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
class Blockchains < Grape::API
helpers ::API::V2::Admin::Helpers
namespace :blockchains do
desc 'Get all blockchains, result is paginated.',
is_array: true,
success: API::V2::Admin::Entities::Blockchain
params do
optional :key,
values: { value: -> { ::Blockchain.pluck(:key) }, message: 'admin.blockchain.blockchain_key_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:key][:desc] }
optional :client,
values: { value: -> { ::Blockchain.clients.map(&:to_s) }, message: 'admin.blockchain.blockchain_client_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:client][:desc] }
optional :status,
values: { value: -> { %w[active disabled] }, message: 'admin.blockchain.blockchain_status_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:status][:desc] }
optional :name,
values: { value: -> { ::Blockchain.pluck(:name) }, message: 'admin.blockchain.blockchain_name_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:name][:desc] }
use :pagination
use :ordering
end
get do
admin_authorize! :read, Blockchain
ransack_params = Helpers::RansackBuilder.new(params)
.eq(:key, :client, :status, :name)
.build
search = ::Blockchain.ransack(ransack_params)
search.sorts = "#{params[:order_by]} #{params[:ordering]}"
present paginate(search.result), with: API::V2::Admin::Entities::Blockchain
end
desc 'Get available blockchain clients.',
is_array: true
get '/clients' do
Blockchain.clients
end
desc 'Get a blockchain.' do
success API::V2::Admin::Entities::Blockchain
end
params do
requires :id,
type: { value: Integer, message: 'admin.blockchain.non_integer_id' },
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:id][:desc] }
end
get '/:id' do
admin_authorize! :read, Blockchain
present Blockchain.find(params[:id]), with: API::V2::Admin::Entities::Blockchain
end
desc 'Get a latest blockchain block.'
params do
requires :id,
type: { value: Integer, message: 'admin.blockchain.non_integer_id' },
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:id][:desc] }
end
get '/:id/latest_block' do
admin_authorize! :read, Blockchain
Blockchain.find(params[:id])&.blockchain_api.latest_block_number
rescue
error!({ errors: ['admin.blockchain.latest_block'] }, 422)
end
desc 'Create new blockchain.' do
success API::V2::Admin::Entities::Blockchain
end
params do
requires :key,
values: { value: -> (v){ v && v.length < 255 }, message: 'admin.blockchain.key_too_long' },
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:key][:desc] }
requires :name,
values: { value: -> (v){ v && v.length < 255 }, message: 'admin.blockchain.name_too_long' },
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:name][:desc] }
requires :client,
values: { value: -> { ::Blockchain.clients.map(&:to_s) }, message: 'admin.blockchain.invalid_client' },
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:client][:desc] }
requires :height,
type: { value: Integer, message: 'admin.blockchain.non_integer_height' },
values: { value: -> (p){ p.try(:positive?) }, message: 'admin.blockchain.non_positive_height' },
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:height][:desc] }
optional :explorer_transaction,
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:explorer_transaction][:desc] }
optional :explorer_address,
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:explorer_address][:desc] }
optional :server,
regexp: { value: URI::regexp, message: 'admin.blockchain.invalid_server' },
desc: -> { 'Blockchain server url' }
optional :status,
values: { value: %w(active disabled), message: 'admin.blockchain.invalid_status' },
default: 'active',
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:status][:desc] }
optional :min_confirmations,
type: { value: Integer, message: 'admin.blockchain.non_integer_min_confirmations' },
values: { value: -> (p){ p.try(:positive?) }, message: 'admin.blockchain.non_positive_min_confirmations' },
default: 6,
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:min_confirmations][:desc] }
end
post '/new' do
admin_authorize! :create, Blockchain
blockchain = Blockchain.new(declared(params))
if blockchain.save
present blockchain, with: API::V2::Admin::Entities::Blockchain
status 201
else
body errors: blockchain.errors.full_messages
status 422
end
end
desc 'Update blockchain.' do
success API::V2::Admin::Entities::Blockchain
end
params do
requires :id,
type: { value: Integer, message: 'admin.blockchain.non_integer_id' },
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:id][:desc] }
optional :key,
type: String,
values: { value: -> (v){ v.length < 255 }, message: 'admin.blockchain.key_too_long' },
coerce_with: ->(v) { v.strip.downcase },
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:key][:desc] }
optional :name,
values: { value: -> (v){ v.length < 255 }, message: 'admin.blockchain.name_too_long' },
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:name][:desc] }
optional :client,
values: { value: -> { ::Blockchain.clients.map(&:to_s) }, message: 'admin.blockchain.invalid_client' },
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:client][:desc] }
optional :server,
regexp: { value: URI::regexp, message: 'admin.blockchain.invalid_server' },
desc: -> { 'Blockchain server url' }
optional :height,
type: { value: Integer, message: 'admin.blockchain.non_integer_height' },
values: { value: -> (p){ p.try(:positive?) }, message: 'admin.blockchain.non_positive_height' },
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:height][:desc] }
optional :explorer_transaction,
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:explorer_transaction][:desc] }
optional :explorer_address,
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:explorer_address][:desc] }
optional :status,
values: { value: %w(active disabled), message: 'admin.blockchain.invalid_status' },
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:status][:desc] }
optional :min_confirmations,
type: { value: Integer, message: 'admin.blockchain.non_integer_min_confirmations' },
values: { value: -> (p){ p.try(:positive?) }, message: 'admin.blockchain.non_positive_min_confirmations' },
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:min_confirmations][:desc] }
end
post '/update' do
admin_authorize! :update, Blockchain, params.except(:id)
blockchain = Blockchain.find(params[:id])
if blockchain.update(declared(params, include_missing: false))
present blockchain, with: API::V2::Admin::Entities::Blockchain
else
body errors: blockchain.errors.full_messages
status 422
end
end
desc 'Process blockchain\'s block.' do
success API::V2::Admin::Entities::Blockchain
end
params do
requires :id,
type: { value: Integer, message: 'admin.blockchain.non_integer_id' },
desc: -> { API::V2::Admin::Entities::Blockchain.documentation[:id][:desc] }
requires :block_number,
type: { value: Integer, message: 'admin.blockchain.non_integer_block_number' },
values: { value: -> (p){ p.try(:positive?) }, message: 'admin.blockchain.non_positive_block_number' },
desc: -> { 'The id of a particular block on blockchain' }
end
post '/process_block' do
admin_authorize! :update, Blockchain
blockchain = Blockchain.find(params[:id])
begin
blockchain.blockchain_api.process_block(params[:block_number])
present blockchain, with: API::V2::Admin::Entities::Blockchain
status 201
rescue StandardError => e
Rails.logger.error { "Error: #{e} while processing block #{params[:block_number]} of blockchain id: #{params[:id]}" }
error!({ errors: ['admin.blockchain.process_block'] }, 422)
end
end
end
end
end
end
end

View File

@@ -0,0 +1,234 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
class Currencies < Grape::API
helpers ::API::V2::Admin::Helpers
helpers do
# Collection of shared params, used to
# generate required/optional Grape params.
OPTIONAL_CURRENCY_PARAMS ||= {
name: { desc: -> { API::V2::Admin::Entities::Currency.documentation[:name][:desc] } },
deposit_fee: {
type: { value: BigDecimal, message: 'admin.currency.non_decimal_deposit_fee' },
values: { value: -> (p){ p >= 0 }, message: 'admin.currency.invalid_deposit_fee' },
default: 0.0,
desc: -> { API::V2::Admin::Entities::Currency.documentation[:deposit_fee][:desc] }
},
min_deposit_amount: {
type: { value: BigDecimal, message: 'admin.currency.min_deposit_amount' },
values: { value: -> (p){ p >= 0 }, message: 'admin.currency.min_deposit_amount' },
default: 0.0,
desc: -> { API::V2::Admin::Entities::Currency.documentation[:min_deposit_amount][:desc] }
},
min_collection_amount: {
type: { value: BigDecimal, message: 'admin.currency.non_decimal_min_collection_amount' },
values: { value: -> (p){ p >= 0 }, message: 'admin.currency.invalid_min_collection_amount' },
default: 0.0,
desc: -> { API::V2::Admin::Entities::Currency.documentation[:min_collection_amount][:desc] }
},
withdraw_fee: {
type: { value: BigDecimal, message: 'admin.currency.non_decimal_withdraw_fee' },
values: { value: -> (p){ p >= 0 }, message: 'admin.currency.ivalid_withdraw_fee' },
default: 0.0,
desc: -> { API::V2::Admin::Entities::Currency.documentation[:withdraw_fee][:desc] }
},
min_withdraw_amount: {
type: { value: BigDecimal, message: 'admin.currency.non_decimal_min_withdraw_amount' },
values: { value: -> (p){ p >= 0 }, message: 'admin.currency.invalid_min_withdraw_amount' },
default: 0.0,
desc: -> { API::V2::Admin::Entities::Currency.documentation[:min_withdraw_amount][:desc] }
},
withdraw_limit_24h: {
type: { value: BigDecimal, message: 'admin.currency.non_decimal_withdraw_limit_24h' },
values: { value: -> (p){ p >= 0 }, message: 'admin.currency.invalid_withdraw_limit_24h' },
default: 0.0,
desc: -> { API::V2::Admin::Entities::Currency.documentation[:withdraw_limit_24h][:desc] }
},
withdraw_limit_72h: {
type: { value: BigDecimal, message: 'admin.currency.non_decimal_withdraw_limit_72h' },
values: { value: -> (p){ p >= 0 }, message: 'admin.currency.invalid_withdraw_limit_72h' },
default: 0.0,
desc: -> { API::V2::Admin::Entities::Currency.documentation[:withdraw_limit_72h][:desc] }
},
options: {
type: { value: JSON, message: 'admin.currency.non_json_options' },
default: {},
desc: -> { API::V2::Admin::Entities::Currency.documentation[:options][:desc] }
},
visible: {
type: { value: Boolean, message: 'admin.currency.non_boolean_visible' },
default: true,
desc: -> { API::V2::Admin::Entities::Currency.documentation[:visible][:desc] }
},
deposit_enabled: {
type: { value: Boolean, message: 'admin.currency.non_boolean_deposit_enabled' },
default: true,
desc: -> { API::V2::Admin::Entities::Currency.documentation[:deposit_enabled][:desc] }
},
withdrawal_enabled: {
type: { value: Boolean, message: 'admin.currency.non_boolean_withdrawal_enabled' },
default: true,
desc: -> { API::V2::Admin::Entities::Currency.documentation[:withdrawal_enabled][:desc] }
},
precision: {
type: { value: Integer, message: 'admin.currency.non_integer_base_precision' },
default: 8,
desc: -> { API::V2::Admin::Entities::Currency.documentation[:precision][:desc] }
},
price: {
type: { value: BigDecimal, message: 'admin.currency.non_decimal_price' },
desc: -> { API::V2::Admin::Entities::Currency.documentation[:price][:desc] }
},
icon_url: { desc: -> { API::V2::Admin::Entities::Currency.documentation[:icon_url][:desc] } },
description: { desc: -> { API::V2::Admin::Entities::Currency.documentation[:description][:desc] } },
homepage: { desc: -> { API::V2::Admin::Entities::Currency.documentation[:homepage][:desc] } },
}
params :create_currency_params do
OPTIONAL_CURRENCY_PARAMS.each do |key, params|
optional key, params
end
end
params :update_currency_params do
OPTIONAL_CURRENCY_PARAMS.each do |key, params|
optional key, params.except(:default)
end
end
end
desc 'Get list of currencies',
is_array: true,
success: API::V2::Admin::Entities::Currency
params do
use :currency_type
use :pagination
optional :ordering,
values: { value: %w(asc desc), message: 'admin.pagination.invalid_ordering' },
default: 'asc',
desc: 'If set, returned values will be sorted in specific order, defaults to \'asc\'.'
optional :order_by,
default: 'position',
desc: 'Name of the field, which result will be ordered by.'
optional :deposit_enabled,
type: { value: Boolean, message: 'admin.currency.non_boolean_deposit_enabled' },
desc: -> { API::V2::Admin::Entities::Currency.documentation[:deposit_enabled][:desc] }
optional :withdrawal_enabled,
type: { value: Boolean, message: 'admin.currency.non_boolean_withdrawal_enabled' },
desc: -> { API::V2::Admin::Entities::Currency.documentation[:withdrawal_enabled][:desc] }
optional :visible,
type: { value: Boolean, message: 'admin.currency.non_boolean_visible' },
desc: -> { API::V2::Admin::Entities::Currency.documentation[:visible][:desc] }
end
get '/currencies' do
admin_authorize! :read, Currency
ransack_params = Helpers::RansackBuilder.new(params)
.eq(:type, :deposit_enabled, :withdrawal_enabled, :visible)
.with_daterange
.build
search = Currency.ransack(ransack_params)
search.sorts = "#{params[:order_by]} #{params[:ordering]}"
present paginate(search.result), with: API::V2::Admin::Entities::Currency
end
desc 'Get a currency.' do
success API::V2::Admin::Entities::Currency
end
params do
requires :code,
type: String,
values: { value: -> { Currency.codes(bothcase: true) }, message: 'admin.currency.doesnt_exist'},
desc: -> { API::V2::Admin::Entities::Currency.documentation[:code][:desc] }
end
get '/currencies/:code', requirements: { code: /[\w\.\-]+/ } do
admin_authorize! :read, Currency
present Currency.find(params[:code]), with: API::V2::Admin::Entities::Currency
end
desc 'Create new currency.' do
success API::V2::Admin::Entities::Currency
end
params do
use :create_currency_params
requires :code,
desc: -> { API::V2::Admin::Entities::Currency.documentation[:code][:desc] }
optional :type,
values: { value: ::Currency.types.map(&:to_s), message: 'admin.currency.invalid_type' },
default: 'coin',
desc: -> { API::V2::Admin::Entities::Currency.documentation[:type][:desc] }
optional :base_factor,
type: { value: Integer, message: 'admin.currency.non_integer_base_factor' },
desc: -> { API::V2::Admin::Entities::Currency.documentation[:base_factor][:desc] }
optional :position,
type: { value: Integer, message: 'admin.currency.non_integer_position' },
desc: -> { API::V2::Admin::Entities::Currency.documentation[:position][:desc] }
optional :subunits,
type: { value: Integer, message: 'admin.currency.non_integer_subunits' },
values: { value: (0..18), message: 'admin.currency.invalid_subunits' },
desc: -> { API::V2::Admin::Entities::Currency.documentation[:subunits][:desc] }
given type: ->(val) { val == 'coin' } do
optional :blockchain_key,
values: { value: -> { ::Blockchain.pluck(:key) }, message: 'admin.currency.blockchain_key_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Currency.documentation[:blockchain_key][:desc] }
optional :parent_id,
values: { value: -> { Currency.coins_without_tokens.pluck(:id).map(&:to_s) }, message: 'admin.currency.parent_id_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Currency.documentation[:parent_id][:desc] }
end
mutually_exclusive :base_factor, :subunits, message: 'admin.currency.one_of_base_factor_subunits_fields'
end
post '/currencies/new' do
admin_authorize! :create, Currency
currency = Currency.new(declared(params, include_missing: false))
if currency.save
present currency, with: API::V2::Admin::Entities::Currency
status 201
else
body errors: currency.errors.full_messages
status 422
end
end
desc 'Update currency.' do
success API::V2::Admin::Entities::Currency
end
params do
use :update_currency_params
requires :code,
values: { value: -> { ::Currency.codes }, message: 'admin.currency.doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Currency.documentation[:code][:desc] }
optional :position,
type: { value: Integer, message: 'admin.currency.non_integer_position' },
values: { value: -> (p){ p >= ::Currency::TOP_POSITION }, message: 'admin.currency.invalid_position' },
desc: -> { API::V2::Admin::Entities::Currency.documentation[:position][:desc] }
optional :blockchain_key,
values: { value: -> { ::Blockchain.pluck(:key) }, message: 'admin.currency.blockchain_key_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Currency.documentation[:blockchain_key][:desc] }
given code: -> (val) { val.in?(Currency.coins.pluck(:code).map(&:to_s)) } do
optional :parent_id,
values: { value: -> { Currency.coins_without_tokens.pluck(:id).map(&:to_s) }, message: 'admin.currency.parent_id_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Currency.documentation[:parent_id][:desc] }
end
end
post '/currencies/update' do
admin_authorize! :update, Currency, params.except(:code)
currency = Currency.find(params[:code])
if currency.update(declared(params, include_missing: false))
present currency, with: API::V2::Admin::Entities::Currency
else
body errors: currency.errors.full_messages
status 422
end
end
end
end
end
end

View File

@@ -0,0 +1,133 @@
# frozen_string_literal: true
#
# this file follow withdraw_limits.rb
#
module API
module V2
module Admin
class DepositLimits < Grape::API
helpers ::API::V2::Admin::Helpers
# list of deposit limits
desc 'Returns deposit limits table as paginated collection',
is_array: true,
success: API::V2::Admin::Entities::DepositLimit
params do
optional :group,
type: String,
desc: -> { API::V2::Entities::DepositLimit.documentation[:group][:desc] },
coerce_with: ->(c) { c.strip.downcase }
optional :kyc_level,
type: String,
desc: -> { API::V2::Entities::DepositLimit.documentation[:kyc_level][:desc] }
optional :kind,
values: { value: ->(v) { (Array.wrap(v) - DepositLimit.kind.values).blank? }, message: 'limit.kind.invalid' },
desc: -> { API::V2::Entities::DepositLimit.documentation[:kind][:desc] }
use :pagination
use :ordering
end
get '/deposit_limits' do
admin_authorize! :read, DepositLimit
ransack_params = Helpers::RansackBuilder.new(params)
.eq(:group, :kyc_level, :kind)
.build
search = DepositLimit.ransack(ransack_params)
search.sorts = "#{params[:order_by]} #{params[:ordering]}"
present paginate(search.result), with: API::V2::Entities::DepositLimit
end
# create deposit limit
desc 'It creates deposit limits record',
success: API::V2::Entities::DepositLimit
params do
requires :limit_24_hour,
type: { value: BigDecimal, message: 'admin.deposit_limit.non_decimal_limit_24_hour' },
values: { value: ->(p) { p && p >= 0 }, message: 'admin.deposit_limit.invalid_limit_24_hour' },
desc: -> { API::V2::Entities::DepositLimit.documentation[:limit_24_hour][:desc] }
requires :limit_1_month,
type: { value: BigDecimal, message: 'admin.deposit_limit.non_decimal_limit_1_month' },
values: { value: ->(p) { p && p >= 0 }, message: 'admin.deposit_limit.invalid_limit_1_month' },
desc: -> { API::V2::Entities::DepositLimit.documentation[:limit_1_month][:desc] }
requires :kind,
values: { value: ->(v) { (Array.wrap(v) - DepositLimit.kind.values).blank? }, message: 'limit.kind.invalid' },
desc: -> { API::V2::Entities::DepositLimit.documentation[:kind][:desc] }
optional :group,
type: String,
default: ::DepositLimit::ANY,
desc: -> { API::V2::Entities::DepositLimit.documentation[:group][:desc] }
optional :kyc_level,
type: String,
default: ::DepositLimit::ANY,
desc: -> { API::V2::Entities::DepositLimit.documentation[:kyc_level][:desc] }
end
post '/deposit_limits' do
admin_authorize! :create, DepositLimit
deposit_limit = ::DepositLimit.new(declared(params))
if deposit_limit.save
present deposit_limit, with: API::V2::Entities::DepositLimit
status 201
else
body errors: deposit_limit.errors.full_messages
status 422
end
end
# update deposit limit
desc 'It updates deposit limits record',
success: API::V2::Entities::DepositLimit
params do
requires :id,
type: { value: Integer, message: 'admin.deposit_limit.non_integer_id' },
desc: -> { API::V2::Entities::DepositLimit.documentation[:id][:desc] }
optional :limit_24_hour,
type: { value: BigDecimal, message: 'admin.deposit_limit.non_decimal_limit_24_hour' },
values: { value: ->(p) { p && p >= 0 }, message: 'admin.deposit_limit.invalid_limit_24_hour' },
desc: -> { API::V2::Entities::DepositLimit.documentation[:limit_24_hour][:desc] }
optional :limit_1_month,
type: { value: BigDecimal, message: 'admin.deposit_limit.non_decimal_limit_1_month' },
values: { value: ->(p) { p && p >= 0 }, message: 'admin.deposit_limit.invalid_limit_1_month' },
desc: -> { API::V2::Entities::DepositLimit.documentation[:limit_1_month][:desc] }
optional :kind,
values: { value: ->(v) { (Array.wrap(v) - DepositLimit.kind.values).blank? }, message: 'limit.kind.invalid' },
desc: -> { API::V2::Entities::DepositLimit.documentation[:kind][:desc] }
optional :kyc_level,
type: String,
desc: -> { API::V2::Entities::DepositLimit.documentation[:kyc_level][:desc] }
optional :group,
type: String,
coerce_with: ->(c) { c.strip.downcase },
desc: -> { API::V2::Entities::DepositLimit.documentation[:group][:desc] }
end
put '/deposit_limits' do
admin_authorize! :update, DepositLimit
deposit_limit = ::DepositLimit.find(params[:id])
if deposit_limit.update(declared(params, include_missing: false))
present deposit_limit, with: API::V2::Entities::DepositLimit
else
body errors: deposit_limit.errors.full_messages
status 422
end
end
# delete deposit limit
desc 'It deletes deposit limits record',
success: API::V2::Entities::DepositLimit
params do
requires :id,
type: { value: Integer, message: 'admin.deposit_limit.non_integer_id' },
desc: -> { API::V2::Entities::DepositLimit.documentation[:id][:desc] }
end
delete '/deposit_limits/:id' do
admin_authorize! :delete, DepositLimit
present DepositLimit.destroy(params[:id]), with: API::V2::Entities::DepositLimit
end
end
end
end
end

View File

@@ -0,0 +1,180 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
class Deposits < Grape::API
helpers ::API::V2::Admin::Helpers
desc 'Get all deposits, result is paginated.',
is_array: true,
success: API::V2::Admin::Entities::Deposit
params do
optional :state,
values: { value: -> { ::Deposit.aasm.states.map(&:name).map(&:to_s) }, message: 'admin.deposit.invalid_state' },
desc: -> { API::V2::Admin::Entities::Deposit.documentation[:state][:desc] }
optional :id,
type: Integer,
desc: -> { API::V2::Admin::Entities::Deposit.documentation[:id][:desc] }
optional :txid,
desc: -> { API::V2::Admin::Entities::Deposit.documentation[:txid][:desc] }
optional :address,
desc: -> { API::V2::Admin::Entities::Deposit.documentation[:address][:desc] }
optional :tid,
desc: -> { API::V2::Admin::Entities::Deposit.documentation[:tid][:desc] }
optional :email,
desc: -> { API::V2::Admin::Entities::Deposit.documentation[:email][:desc] }
use :uid
use :currency
use :currency_type
use :date_picker
use :pagination
use :ordering
end
get '/deposits' do
admin_authorize! :read, Deposit
ransack_params = Helpers::RansackBuilder.new(params)
.eq(:id, :txid, :tid, :address)
.translate(state: :aasm_state, uid: :member_uid, currency: :currency_id, email: :member_email)
.with_daterange
.merge(type_eq: params[:type].present? ? "Deposits::#{params[:type].capitalize}" : nil)
.build
search = Deposit.ransack(ransack_params)
search.sorts = "#{params[:order_by]} #{params[:ordering]}"
present paginate(search.result), with: API::V2::Admin::Entities::Deposit
end
desc 'Take an action on the deposit.',
success: API::V2::Admin::Entities::Deposit
params do
requires :id,
type: Integer,
desc: -> { API::V2::Admin::Entities::Deposit.documentation[:id][:desc] }
requires :action,
type: String,
values: { value: -> { ::Deposit.aasm.events.map(&:name).map(&:to_s) }, message: 'admin.deposit.invalid_action' },
desc: "Valid actions are #{::Deposit.aasm.events.map(&:name)}."
given action: ->(val) { val == 'process' } do
optional :fees,
type: Boolean,
default: false,
desc: 'Process deposit collection with collecting fees or not'
end
end
post '/deposits/actions' do
admin_authorize! :update, Deposit
deposit = Deposit.find(params[:id])
if deposit.public_send("may_#{params[:action]}?")
deposit.public_send("#{params[:action]}!")
present deposit, with: API::V2::Admin::Entities::Deposit
else
body errors: ["admin.depodit.cannot_#{params[:action]}"]
status 422
end
end
desc 'Creates new fiat deposit .',
success: API::V2::Admin::Entities::Deposit
params do
requires :uid,
values: { value: -> (v) { Member.exists?(uid: v) }, message: 'admin.deposit.user_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Deposit.documentation[:uid][:desc] }
requires :currency,
values: { value: -> { Currency.fiats.codes(bothcase: true) }, message: 'admin.deposit.currency_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Deposit.documentation[:currency][:desc] }
requires :amount,
type: { value: BigDecimal, message: 'admin.deposit.non_decimal_amount' },
desc: -> { API::V2::Admin::Entities::Deposit.documentation[:amount][:desc] }
optional :tid,
desc: -> { API::V2::Admin::Entities::Deposit.documentation[:tid][:desc] }
end
post '/deposits/new' do
admin_authorize! :create, ::Deposits::Fiat
declared_params = declared(params, include_missing: false)
member = Member.find_by(uid: declared_params[:uid])
currency = Currency.find(declared_params[:currency])
data = { member: member, currency: currency }.merge!(declared_params.slice(:amount, :tid))
deposit = ::Deposits::Fiat.new(data)
if deposit.save
present deposit, with: API::V2::Admin::Entities::Deposit
status 201
else
body errors: deposit.errors.full_messages
status 422
end
end
desc 'Creates new crypto refund',
success: API::V2::Admin::Entities::Refund
params do
requires :id,
type: { value: Integer, message: 'admin.deposit.non_integer_type' },
desc: -> { 'Deposit id' }
requires :address,
desc: -> { API::V2::Admin::Entities::Refund.documentation[:address][:desc] }
end
post '/deposits/:id/refund' do
admin_authorize! :wrrie, Deposit
deposit = Deposit.find(params[:id])
refund = ::Refund.new(deposit: deposit, address: params[:address])
if refund.save
present refund, with: API::V2::Admin::Entities::Refund
else
body errors: refund.errors.full_messages
status 422
end
end
desc 'Returns deposit address for account you want to deposit to by currency and uid.',
success: API::V2::Admin::Entities::Deposit
params do
requires :uid,
values: { value: -> (v) { Member.exists?(uid: v) }, message: 'admin.deposit.user_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Deposit.documentation[:uid][:desc] }
requires :currency,
values: { value: -> { Currency.codes }, message: 'admin.deposit.currency_doesnt_exist' },
as: :currency_id,
desc: -> { API::V2::Admin::Entities::Deposit.documentation[:currency][:desc] }
given :currency_id do
optional :address_format,
type: String,
values: { value: -> { %w[legacy cash] }, message: 'admin.deposit.invalid_address_format' },
validate_currency_address_format: { value: true, prefix: 'admin.deposit' },
desc: 'Address format legacy/cash'
end
end
post '/deposit_address' do
admin_authorize! :create, PaymentAddress
member = Member.find_by!(uid: params[:uid])
currency = Currency.find_by!(id: params[:currency_id])
wallet = Wallet.deposit_wallet(currency.id)
unless wallet.present?
error!({ errors: ['admin.deposit.wallet_not_found'] }, 422)
end
if currency.deposit_enabled
payment_address = member.payment_address(wallet.id)
present payment_address, with: API::V2::Entities::PaymentAddress, address_format: params[:address_format]
status 201
else
body errors: ["admin.deposit.deposit_disabled"]
status 422
end
end
end
end
end
end

101
app/api/v2/admin/engines.rb Normal file
View File

@@ -0,0 +1,101 @@
# frozen_string_literal: true
module API
module V2
module Admin
class Engines < Grape::API
helpers ::API::V2::Admin::Helpers
desc 'Get all engine, result is paginated.',
is_array: true,
success: API::V2::Admin::Entities::Engine
params do
use :pagination
use :ordering
end
get '/engines' do
admin_authorize! :read, ::Engine
result = ::Engine.order(params[:order_by] => params[:ordering])
present paginate(result), with: API::V2::Admin::Entities::Engine
end
desc 'Get engine.' do
success API::V2::Admin::Entities::Engine
end
params do
requires :id,
type: String,
desc: -> { API::V2::Admin::Entities::Engine.documentation[:id][:desc] }
end
get '/engines/:id' do
admin_authorize! :read, ::Engine
present ::Engine.find(params[:id]), with: API::V2::Admin::Entities::Engine
end
desc 'Create new engine.' do
success API::V2::Admin::Entities::Engine
end
params do
requires :name,
desc: -> { API::V2::Admin::Entities::Engine.documentation[:name][:desc] },
values: { value: ->(v) { !v.in?(::Engine.pluck(:name)) }, message: 'admin.engine.duplicate_name' }
requires :driver,
desc: -> { API::V2::Admin::Entities::Engine.documentation[:driver][:desc] }
optional :uid,
desc: -> { API::V2::Admin::Entities::Engine.documentation[:uid][:desc] }
optional :key,
desc: -> { 'Credentials for remote engine' }
optional :secret,
desc: -> { 'Credentials for remote engine' }
optional :data,
desc: -> { 'Metadata for engine' }
end
post '/engines/new' do
admin_authorize! :create, ::Engine
engine = ::Engine.new(declared(params))
if engine.save
present engine, with: API::V2::Admin::Entities::Engine
status 201
else
body errors: engine.errors.full_messages
status 422
end
end
desc 'Update engine' do
success API::V2::Admin::Entities::Engine
end
params do
requires :id,
desc: -> { API::V2::Admin::Entities::Engine.documentation[:id][:desc] }
optional :name,
desc: -> { API::V2::Admin::Entities::Engine.documentation[:name][:desc] }
optional :driver,
desc: -> { API::V2::Admin::Entities::Engine.documentation[:driver][:desc] }
optional :key,
desc: -> { 'Credentials for remote engine' }
optional :secret,
desc: -> { 'Credentials for remote engine' }
optional :state,
values: { value: ::Engine::STATES.values, message: 'admin.engine.invalid_state' },
default: 1,
desc: -> { API::V2::Admin::Entities::Engine.documentation[:state][:desc] }
end
post '/engines/update' do
admin_authorize! :update, ::Engine
engine = ::Engine.find(params[:id])
if engine.update(declared(params, include_missing: false))
present engine, with: API::V2::Admin::Entities::Engine
else
body errors: engine.errors.full_messages
status 422
end
end
end
end
end
end

View File

@@ -0,0 +1,169 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
module Entities
class Adjustment < API::V2::Entities::Base
expose(
:id,
documentation: {
type: Integer,
desc: 'Unique adjustment identifier in database.'
}
)
expose(
:reason,
documentation: {
type: String,
desc: 'Adjustment reason.'
}
)
expose(
:description,
documentation: {
type: String,
desc: 'Adjustment description.'
}
)
expose(
:category,
documentation: {
type: String,
desc: 'Adjustment category'
}
)
expose(
:amount,
documentation: {
type: String,
desc: 'Adjustment amount.'
}
)
expose(
:validator_uid,
documentation: {
type: Integer,
desc: 'Unique adjustment validator identifier in database.'
},
if: ->(adjustment, _options) { adjustment.validator }
) do |adjustment, _options|
adjustment.validator.uid
end
expose(
:creator_uid,
documentation: {
type: Integer,
desc: 'Unique adjustment creator identifier in database.'
},
) do |adjustment, _options|
adjustment.creator.uid
end
expose(
:currency_id,
as: :currency,
documentation: {
type: String,
desc: 'Adjustment currency ID.'
}
)
expose(
:asset,
using: API::V2::Admin::Entities::Operation,
if: ->(adjustment, _options) { adjustment.fetch_asset }
) do |adjustment, _options|
adjustment.fetch_asset
end
expose(
:liability,
using: API::V2::Admin::Entities::Operation,
if: ->(adjustment, _options) { adjustment.fetch_liability }
) do |adjustment, _options|
adjustment.fetch_liability
end
expose(
:revenue,
using: API::V2::Admin::Entities::Operation,
if: ->(adjustment, _options) { adjustment.fetch_revenue }
) do |adjustment, _options|
adjustment.fetch_revenue
end
expose(
:expense,
using: API::V2::Admin::Entities::Operation,
if: ->(adjustment, _options) { adjustment.fetch_expense }
) do |adjustment, _options|
adjustment.fetch_expense
end
expose(
:state,
documentation: {
type: String,
desc: 'Adjustment\'s state.'
}
)
expose(
:asset_account_code,
documentation: {
type: Integer,
desc: 'Adjustment asset account code.'
}
)
expose(
:receiving_account_code,
documentation: {
type: String,
desc: 'Adjustment receiving account code.'
}
) do |adjustment, _options|
::Operations.split_account_number(account_number: adjustment.receiving_account_number)[:code]
end
expose(
:receiving_member_uid,
documentation: {
type: String,
desc: 'Adjustment receiving member uid.'
},
if: ->(adjustment, _options) { adjustment.fetch_liability.present? }
) do |adjustment, _options|
::Operations.split_account_number(account_number: adjustment.receiving_account_number)[:member_uid]
end
expose(
:created_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'The datetime when operation was created.'
}
)
expose(
:updated_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'The datetime when operation was updated.'
}
)
end
end
end
end
end

View File

@@ -0,0 +1,20 @@
# frozen_string_literal: true
module API
module V2
module Admin
module Entities
class Beneficiary < API::V2::Entities::Beneficiary
expose(
:data,
documentation: {
desc: 'Bank Account details for fiat Beneficiary in JSON format.'\
'For crypto it\'s blockchain address.',
type: JSON
}
)
end
end
end
end
end

View File

@@ -0,0 +1,102 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
module Entities
class Blockchain < API::V2::Entities::Base
expose(
:id,
documentation:{
type: Integer,
desc: 'Unique blockchain identifier in database.'
}
)
expose(
:key,
documentation:{
type: String,
desc: 'Unique key to identify blockchain.'
}
)
expose(
:name,
documentation:{
type: String,
desc: 'A name to identify blockchain.'
}
)
expose(
:client,
documentation:{
type: String,
desc: 'Integrated blockchain client.'
}
)
expose(
:height,
documentation:{
type: Integer,
desc: 'The number of blocks preceding a particular block on blockchain.'
}
)
expose(
:explorer_address,
documentation:{
type: String,
desc: 'Blockchain explorer address template.'
}
)
expose(
:explorer_transaction,
documentation:{
type: String,
desc: 'Blockchain explorer transaction template.'
}
)
expose(
:min_confirmations,
documentation:{
type: Integer,
desc: 'Minimum number of confirmations.'
}
)
expose(
:status,
documentation:{
type: String,
desc: 'Blockchain status (active/disabled).'
}
)
expose(
:created_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'Blockchain created time in iso8601 format.'
}
)
expose(
:updated_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'Blockchain updated time in iso8601 format.'
}
)
end
end
end
end
end

View File

@@ -0,0 +1,123 @@
# enco ding: UTF-8
# froz en_string_literal: true
module API
module V2
module Admin
module Entities
class Currency < API::V2::Entities::Currency
unexpose(:id)
expose(
:code,
documentation: {
desc: 'Unique currency code.',
type: String
}
)
expose(
:blockchain_key,
documentation: {
type: String,
desc: 'Associated blockchain key which will perform transactions synchronization for currency.'
},
if: -> (currency){ currency.coin? }
)
expose(
:parent_id,
documentation: {
desc: 'Parent currency id.',
type: String
},
if: -> (currency){ currency.token? }
)
expose(
:min_collection_amount,
documentation: {
type: BigDecimal,
desc: 'Minimal collection amount.'
}
)
expose(
:position,
documentation: {
type: Integer,
desc: 'Currency position.'
}
)
expose(
:visible,
documentation: {
type: String,
desc: 'Currency display status (true/false).'
}
)
expose(
:base_factor,
documentation: {
type: Integer,
desc: 'Currency base factor.'
}
)
expose(
:subunits,
documentation: {
type: Integer,
desc: 'Fraction of the basic monetary unit.'
}
) { |currency| currency.subunits }
expose(
:options,
documentation: {
type: JSON,
desc: 'Currency options.'
},
if: -> (currency){ currency.coin? }
)
expose(
:precision,
documentation: {
type: Integer,
desc: 'Currency precision.'
}
)
expose(
:price,
documentation: {
type: BigDecimal,
desc: 'Currency price.'
}
)
expose(
:created_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'Currency created time in iso8601 format.'
}
)
expose(
:updated_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'Currency updated time in iso8601 format.'
}
)
end
end
end
end
end

View File

@@ -0,0 +1,108 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
module Entities
class Deposit < API::V2::Entities::Deposit
expose(
:member_id,
as: :member,
documentation: {
type: String,
desc: 'The member id.'
}
)
expose(
:uid,
documentation: {
type: String,
desc: 'Deposit member uid.'
}
)
expose(
:email,
documentation: {
type: String,
desc: 'The deposit member email.'
}
) { |d| d.member.email }
expose(
:address,
documentation: {
type: String,
desc: 'Deposit blockchain address.'
},
if: ->(deposit) { deposit.currency.coin? }
)
expose(
:txout,
documentation: {
type: Integer,
desc: 'Deposit blockchain transaction output.'
},
if: ->(deposit) { deposit.currency.coin? }
)
expose(
:block_number,
documentation: {
type: Integer,
desc: 'Deposit blockchain block number.'
},
if: ->(deposit) { deposit.currency.coin? }
)
expose(
:type,
documentation: {
type: String,
desc: 'Deposit type (fiat or coin).'
}
) { |d| d.currency.fiat? ? :fiat : :coin }
expose(
:tid,
documentation: {
type: String,
desc: 'Deposit tid.'
}
)
expose(
:spread,
documentation: {
type: String,
desc: 'Deposit collection spread.'
},
if: -> (deposit) { !deposit.spread.empty? }
)
expose(
:updated_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'The datetime when deposit was updated.'
}
)
expose(
:completed_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'The datetime when deposit was completed.'
},
if: ->(deposit) { deposit.completed? }
)
end
end
end
end
end

View File

@@ -0,0 +1,51 @@
# frozen_string_literal: true
module API
module V2
module Admin
module Entities
class Engine < API::V2::Entities::Base
expose(
:id,
documentation: {
type: Integer,
desc: 'Engine uniq id'
}
)
expose(
:name,
documentation: {
type: String,
desc: 'Engine name'
}
)
expose(
:driver,
documentation: {
type: String,
desc: 'Engine driver'
}
)
expose(
:uid,
documentation: {
type: String,
desc: 'Owner of a engine'
}
)
expose(
:state,
documentation: {
type: String,
desc: 'Engine state'
}
)
end
end
end
end
end

View File

@@ -0,0 +1,20 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
module Entities
class InternalTransfer < API::V2::Entities::InternalTransfer
expose(
:id,
documentation: {
type: Integer,
desc: 'Internal transfer uniq id'
}
)
end
end
end
end
end

View File

@@ -0,0 +1,54 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
module Entities
class Market < API::V2::Entities::Market
expose(
:engine_id,
documentation: {
type: Integer,
desc: 'Engine id for this market.'
}
)
expose(
:position,
documentation: {
type: Integer,
desc: 'Market position.'
}
)
expose(
:data,
documentation: {
type: JSON,
desc: 'Market additional data.'
}
)
expose(
:created_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'Market created time in iso8601 format.'
}
)
expose(
:updated_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'Market updated time in iso8601 format.'
}
)
end
end
end
end
end

View File

@@ -0,0 +1,104 @@
# frozen_string_literal: true
module API
module V2
module Admin
module Entities
class Member < API::V2::Entities::Member
expose(
:id,
documentation: {
type: Integer,
desc: 'Unique member identifier in database.'
}
)
expose(
:level,
documentation: {
type: Integer,
desc: 'Member\'s level.'
}
)
expose(
:role,
documentation: {
type: String,
desc: 'Member\'s role.'
}
)
expose(
:group,
documentation: {
type: String,
desc: 'Member\'s group.'
}
)
expose(
:state,
documentation: {
type: String,
desc: 'Member\'s state.'
}
)
expose(
:created_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'Member created time in iso8601 format.'
}
)
expose(
:updated_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'Member updated time in iso8601 format.'
}
)
expose(
:beneficiaries,
using: API::V2::Admin::Entities::Beneficiary,
documentation: {
type: 'API::V2::Admin::Entities::Beneficiary',
is_array: true,
desc: 'Member Beneficiary.'
}
) do |m|
m.beneficiaries
end
expose(
:accounts,
using: API::V2::Entities::Account,
documentation: {
type: 'API::V2::Entities::Account',
is_array: true,
desc: 'Member accounts.'
}
) do |m|
m.accounts.includes(:currency)
end
expose(
:payment_addresses,
as: :deposit_addresses,
using: API::V2::Entities::PaymentAddress,
documentation: {
type: 'API::V2::Entities::PaymentAddress',
is_array: true,
desc: 'Member deposits addresses'
}
)
end
end
end
end
end

View File

@@ -0,0 +1,94 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
module Entities
class Operation < API::V2::Entities::Base
expose(
:id,
documentation: {
type: Integer,
desc: 'Unique operation identifier in database.'
}
)
expose(
:code,
documentation: {
type: String,
desc: 'The Account code which this operation related to.'
}
)
expose(
:currency_id,
as: :currency,
documentation: {
type: String,
desc: 'Operation currency ID.'
}
)
expose(
:credit,
documentation: {
type: String,
desc: 'Operation credit amount.'
}
)
expose(
:debit,
documentation: {
type: String,
desc: 'Operation debit amount.'
}
)
expose(
:uid,
if: ->(operation) { operation.try(:member).try(:uid) },
documentation: {
type: String,
desc: 'The shared user ID.'
}) { |operation| operation.try(:member).try(:uid) }
expose(
:account_kind,
documentation: {
type: String,
desc: 'Operation\'s account kind (locked or main).'
}
) { |operation| operation.account.kind }
expose(
:reference_id,
as: :rid,
documentation: {
type: String,
desc: 'The id of operation reference.'
}
)
expose(
:reference_type,
documentation: {
type: String,
desc: 'The type of operations.'
}) { |operation| operation.reference_type.downcase if operation.reference_type.present? }
expose(
:created_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'The datetime when operation was created.'
}
)
end
end
end
end
end

View File

@@ -0,0 +1,30 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
module Entities
class Order < API::V2::Entities::Order
unexpose(:trades)
expose(
:email,
documentation: {
type: String,
desc: 'The shared user email.'
}
) { |w| w.member.email }
expose(
:uid,
documentation: {
type: String,
desc: 'The shared user ID.'
}
) { |w| w.member.uid }
end
end
end
end
end

View File

@@ -0,0 +1,33 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
module Entities
class Refund < API::V2::Entities::Base
expose(
:id,
documentation: {
type: Integer,
desc: 'The refund id'
}
)
expose(
:address,
documentation: {
type: String,
desc: 'Refund address'
}
)
expose(
:deposit,
using: API::V2::Admin::Entities::Deposit
)
end
end
end
end
end

View File

@@ -0,0 +1,112 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
module Entities
class Trade < API::V2::Entities::Trade
unexpose(:side)
unexpose(:order_id)
unexpose(:fee_currency)
unexpose(:fee)
unexpose(:fee_amount)
expose(
:maker_order_email,
documentation: {
type: String,
desc: 'Trade maker member email.'
}
) { |trade| trade.maker.email }
expose(
:maker_uid,
documentation: {
type: String,
desc: 'Trade maker member uid.'
}
) { |trade| trade.maker.uid }
expose(
:maker_fee,
documentation: {
type: BigDecimal,
desc: 'Trade maker fee percentage.',
},
if: ->(object, options) { options[:extended] }
) { |trade| trade.maker_order.maker_fee }
expose(
:maker_fee_amount,
documentation: {
type: BigDecimal,
desc: 'Trade maker fee amount.',
}
) { |trade| fee_amount(trade, trade.maker_order) }
expose(
:maker_fee_currency,
documentation: {
type: String,
desc: 'Trade maker fee currency code.'
}
) { |trade| fee_currency(trade.maker_order) }
expose(
:maker_order,
using: API::V2::Admin::Entities::Order,
if: ->(object, options) { options[:extended] }
)
expose(
:taker_order_email,
documentation: {
type: String,
desc: 'Trade taker member email.'
}
) { |trade| trade.taker.email }
expose(
:taker_uid,
documentation: {
type: String,
desc: 'Trade taker member uid.'
}
) { |trade| trade.taker.uid }
expose(
:taker_fee_currency,
documentation: {
type: String,
desc: 'Trade taker fee currency code.'
}
) { |trade| fee_currency(trade.taker_order) }
expose(
:taker_fee,
documentation: {
type: BigDecimal,
desc: 'Trade taker fee percentage.',
},
if: ->(object, options) { options[:extended] }
) { |trade| trade.taker_order.taker_fee }
expose(
:taker_fee_amount,
documentation: {
type: BigDecimal,
desc: 'Trade taker fee amount.',
}
) { |trade| fee_amount(trade, trade.taker_order) }
expose(
:taker_order,
using: API::V2::Admin::Entities::Order,
if: ->(object, options) { options[:extended] }
)
end
end
end
end
end

View File

@@ -0,0 +1,112 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
module Entities
class Wallet < API::V2::Entities::Base
expose(
:id,
documentation:{
type: Integer,
desc: 'Unique wallet identifier in database.'
}
)
expose(
:name,
documentation: {
type: String,
desc: 'Wallet name.'
}
)
expose(
:kind,
documentation: {
type: String,
desc: "Kind of wallet 'deposit','fee','hot','warm' or 'cold'."
}
)
expose(
:currency_ids,
as: :currencies,
documentation: {
is_array: true,
desc: 'Wallet currency code.',
example: -> { ::Currency.visible.codes }
}
)
expose(
:address,
documentation: {
type: String,
desc: 'Wallet address.'
}
)
expose(
:gateway,
documentation: {
type: String,
desc: 'Wallet gateway.'
}
)
expose(
:max_balance,
documentation: {
type: BigDecimal,
desc: 'Wallet max balance.'
}
)
expose(
:balance,
documentation: {
type: BigDecimal,
desc: 'Wallet balance'
}
)
expose(
:blockchain_key,
documentation: {
type: String,
desc: 'Wallet blockchain key.'
}
)
expose(
:status,
documentation: {
type: String,
desc: 'Wallet status (active/disabled).'
}
)
expose(
:created_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'Wallet created time in iso8601 format.'
}
)
expose(
:updated_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'Wallet updated time in iso8601 format.'
}
)
end
end
end
end
end

View File

@@ -0,0 +1,69 @@
# frozen_string_literal: true
module API
module V2
module Admin
module Entities
class WhitelistedSmartContract < API::V2::Entities::Base
expose(
:id,
documentation: {
type: Integer,
desc: 'Unique whitelisted smart contract identifier in database.'
}
)
expose(
:address,
documentation: {
type: String,
desc: 'Whitelisted smart contract address.'
}
)
expose(
:description,
documentation: {
type: String,
desc: 'Whitelisted smart contract description.'
}
)
expose(
:blockchain_key,
documentation: {
type: String,
desc: 'Whitelisted smart contract blockchain key.'
}
)
expose(
:state,
documentation: {
type: String,
desc: 'Whitelisted smart contract status (active/disabled).'
}
)
expose(
:created_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'Whitelisted smart contract created time in iso8601 format.'
}
)
expose(
:updated_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'Whitelisted smart contract updated time in iso8601 format.'
}
)
end
end
end
end
end

View File

@@ -0,0 +1,87 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
module Entities
class Withdraw < API::V2::Entities::Withdraw
expose(
:member_id,
as: :member,
documentation: {
type: String,
desc: 'The member id.'
}
)
expose(
:beneficiary,
using: API::V2::Entities::Beneficiary,
if: ->(withdraw, options) do
options[:with_beneficiary] && withdraw.beneficiary.present?
end
)
expose(
:uid,
documentation: {
type: String,
desc: 'The withdrawal member uid.'
}
) { |w| w.member.uid }
expose(
:email,
documentation: {
type: String,
desc: 'The withdrawal member email.'
}
) { |w| w.member.email }
expose(
:account,
documentation: {
type: String,
desc: 'The account code.'
}
) { |w| w.account.id }
expose(
:block_number,
documentation: {
type: Integer,
desc: 'The withdrawal block_number.'
},
if: ->(w) { w.currency.coin? }
)
expose(
:tid,
documentation: {
type: String,
desc: 'Withdraw tid.'
}
)
expose(
:error,
documentation: {
type: String,
desc: 'Withdraw error.'
},
unless: ->(w) { w.succeed? }
)
expose(
:metadata,
documentation: {
type: String,
desc: 'Optional metadata to be applied to the transaction.'
}
)
end
end
end
end
end

115
app/api/v2/admin/helpers.rb Normal file
View File

@@ -0,0 +1,115 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
module Helpers
extend ::Grape::API::Helpers
class RansackBuilder
# RansackBuilder creates a hash in a format ransack accepts
# eq(:column) generetes a pair column_eq: params[:column]
# translate(:column1 => :column2) generates a pair column2_eq: params[:column1]
# merge allows to append additional selectors in
# build returns prepared hash
attr_reader :build
def initialize(params)
@params = params
@build = {}
end
def merge(opt)
@build.merge!(opt)
self
end
def with_daterange
@build.merge!("#{@params[:range]}_at_gteq" => @params[:from])
@build.merge!("#{@params[:range]}_at_lteq" => @params[:to])
self
end
def translate(opt)
opt.each { |k, v| @build.merge!("#{v}_eq" => @params[k]) }
self
end
def translate_in(opt)
opt.each { |k, v| @build.merge!("#{v}_in" => @params[k]) }
self
end
def in(*keys)
keys.each { |k| @build.merge!("#{k}_in" => @params[k]) }
self
end
def eq(*keys)
keys.each { |k| @build.merge!("#{k}_eq" => @params[k]) }
self
end
end
params :currency_type do
optional :type,
type: String,
values: { value: ::Currency.types.map(&:to_s), message: 'admin.currency.invalid_type' },
desc: -> { API::V2::Admin::Entities::Currency.documentation[:type][:desc] }
end
params :currency do
optional :currency,
values: { value: -> { Currency.codes(bothcase: true) }, message: 'admin.currency.doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Deposit.documentation[:currency][:desc] }
end
params :uid do
optional :uid,
values: { value: -> (v) { Member.exists?(uid: v) }, message: 'admin.user.doesnt_exist' },
desc: -> { API::V2::Entities::Member.documentation[:uid][:desc] }
end
params :pagination do
optional :limit,
type: { value: Integer, message: 'admin.pagination.non_integer_limit' },
values: { value: 1..1000, message: 'admin.pagination.invalid_limit' },
default: 100,
desc: 'Limit the number of returned paginations. Defaults to 100.'
optional :page,
type: { value: Integer, message: 'admin.pagination.non_integer_page' },
allow_blank: false,
default: 1,
desc: 'Specify the page of paginated results.'
end
params :ordering do
optional :ordering,
values: { value: %w(asc desc), message: 'admin.pagination.invalid_ordering' },
default: 'asc',
desc: 'If set, returned values will be sorted in specific order, defaults to \'asc\'.'
optional :order_by,
default: 'id',
desc: 'Name of the field, which result will be ordered by.'
end
params :date_picker do
optional :range,
default: 'created',
values: { value: -> { %w[created updated completed] } },
desc: 'Date range picker, defaults to \'created\'.'
optional :from,
type: { value: Time, message: 'admin.filter.range_from_invalid' },
desc: 'An integer represents the seconds elapsed since Unix epoch.'\
'If set, only entities FROM the time will be retrieved.'
optional :to,
type: { value: Time, message: 'admin.filter.range_to_invalid' },
desc: 'An integer represents the seconds elapsed since Unix epoch.'\
'If set, only entities BEFORE the time will be retrieved.'
end
end
end
end
end

View File

@@ -0,0 +1,51 @@
# frozen_string_literal: true
module API
module V2
module Admin
class InternalTransfers < Grape::API
helpers ::API::V2::Admin::Helpers
desc 'Get all internal transfers.',
is_array: true,
success: API::V2::Admin::Entities::InternalTransfer
params do
optional :sender,
values: { value: -> (v) { Member.where('uid = ? OR username = ?', v, v).present? }, message: 'admin.receiver.doesnt_exist' },
desc: 'Sender uid or username.'
optional :receiver,
values: { value: -> (v) { Member.where('uid = ? OR username = ?', v, v).present? }, message: 'admin.receiver.doesnt_exist' },
desc: 'Receiver uid or username.'
use :currency
use :pagination
use :date_picker
use :ordering
end
get '/internal_transfers' do
admin_authorize! :read, InternalTransfer
if params[:sender].present?
sender = Member.find_by('uid = ? OR username = ?', params[:sender], params[:sender])
params.except!(:sender).merge!(sender_id: sender.id) if sender.present?
end
if params[:receiver].present?
receiver = Member.find_by('uid = ? OR username = ?', params[:receiver], params[:receiver])
params.except!(:receiver).merge!(receiver_id: receiver.id) if receiver.present?
end
ransack_params = Helpers::RansackBuilder.new(params)
.eq(:sender_id, :receiver_id)
.translate_in(currency: :currency_id)
.with_daterange
.build
search = ::InternalTransfer.ransack(ransack_params)
search.sorts = "#{params[:order_by]} #{params[:ordering]}"
present paginate(search.result), with: API::V2::Admin::Entities::InternalTransfer
end
end
end
end
end

171
app/api/v2/admin/markets.rb Normal file
View File

@@ -0,0 +1,171 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
class Markets < Grape::API
helpers ::API::V2::Admin::Helpers
helpers do
# Collection of shared params, used to
# generate required/optional Grape params.
OPTIONAL_MARKET_PARAMS ||= {
amount_precision: {
type: { value: Integer, message: 'admin.market.non_integer_amount_precision' },
values: { value: -> (p){ p && p >= 0 }, message: 'admin.market.invalid_amount_precision' },
default: 4,
desc: -> { API::V2::Admin::Entities::Market.documentation[:amount_precision][:desc] }
},
price_precision: {
type: { value: Integer, message: 'admin.market.non_integer_price_precision' },
values: { value: -> (p){ p && p >= 0 }, message: 'admin.market.invalid_price_precision' },
default: 4,
desc: -> { API::V2::Admin::Entities::Market.documentation[:price_precision][:desc] }
},
max_price: {
type: { value: BigDecimal, message: 'admin.market.non_decimal_max_price' },
values: { value: -> (p){ p >= 0 }, message: 'admin.market.invalid_max_price' },
default: 0.0,
desc: -> { API::V2::Admin::Entities::Market.documentation[:max_price][:desc] }
},
data: {
type: { value: JSON, message: 'admin.market.invalid_data' },
default: {},
desc: -> { API::V2::Admin::Entities::Market.documentation[:data][:desc] }
},
state: {
values: { value: ::Market::STATES, message: 'admin.market.invalid_state' },
default: 'enabled',
desc: -> { API::V2::Admin::Entities::Market.documentation[:state][:desc] }
},
}
params :create_market_params do
OPTIONAL_MARKET_PARAMS.each do |key, params|
optional key, params
end
end
params :update_market_params do
OPTIONAL_MARKET_PARAMS.each do |key, params|
optional key, params.except(:default)
end
end
end
desc 'Get all markets, result is paginated.',
is_array: true,
success: API::V2::Admin::Entities::Market
params do
use :pagination
optional :ordering,
values: { value: %w(asc desc), message: 'admin.pagination.invalid_ordering' },
default: 'asc',
desc: 'If set, returned values will be sorted in specific order, defaults to \'asc\'.'
optional :order_by,
default: 'position',
desc: 'Name of the field, which result will be ordered by.'
end
get '/markets' do
admin_authorize! :read, ::Market
result = ::Market.order(params[:order_by] => params[:ordering])
present paginate(result), with: API::V2::Admin::Entities::Market
end
desc 'Get market.' do
success API::V2::Admin::Entities::Market
end
params do
requires :id,
type: String,
desc: -> { API::V2::Admin::Entities::Market.documentation[:id][:desc] }
end
get '/markets/:id', requirements: { id: /[\w\.\-]+/ } do
admin_authorize! :read, ::Market
present ::Market.find(params[:id]), with: API::V2::Admin::Entities::Market
end
desc 'Create new market.' do
success API::V2::Admin::Entities::Market
end
params do
use :create_market_params
requires :base_currency,
values: { value: -> { ::Currency.ids }, message: 'admin.market.currency_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Market.documentation[:base_unit][:desc] }
requires :quote_currency,
values: { value: -> { ::Currency.ids }, message: 'admin.market.currency_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Market.documentation[:quote_unit][:desc] }
requires :min_price,
type: { value: BigDecimal, message: 'admin.market.non_decimal_min_price' },
values: { value: -> (p){ p && p >= 0 }, message: 'admin.market.invalid_min_price' },
desc: -> { API::V2::Admin::Entities::Market.documentation[:min_price][:desc] }
requires :min_amount,
type: { value: BigDecimal, message: 'admin.market.non_decimal_min_amount' },
values: { value: -> (p){ p && p >= 0 }, message: 'admin.market.invalid_min_amount' },
desc: -> { API::V2::Admin::Entities::Market.documentation[:min_amount][:desc] }
optional :engine_id,
type: { value: Integer, message: 'admin.market.non_integer_engine_id' },
desc: -> { API::V2::Admin::Entities::Market.documentation[:engine_id][:desc] }
optional :position,
type: { value: Integer, message: 'admin.market.non_integer_position' },
desc: -> { API::V2::Admin::Entities::Market.documentation[:position][:desc] }
optional :engine_name,
values: { value: -> { ::Engine.pluck(:name) }, message: 'admin.market.engine_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Engine.documentation[:name][:desc] }
exactly_one_of :engine_id, :engine_name, message: 'admin.market.one_of_engine_id_engine_name_fields'
end
post '/markets/new' do
admin_authorize! :create, ::Market
market = ::Market.new(declared(params, include_missing: false))
if market.save
present market, with: API::V2::Admin::Entities::Market
status 201
else
body errors: market.errors.full_messages
status 422
end
end
desc 'Update market.' do
success API::V2::Admin::Entities::Market
end
params do
use :update_market_params
requires :id,
desc: -> { API::V2::Admin::Entities::Market.documentation[:id][:desc] }
optional :engine_id,
type: { value: Integer, message: 'admin.market.non_integer_engine_id' },
desc: -> { API::V2::Admin::Entities::Market.documentation[:engine_id][:desc] }
optional :position,
type: { value: Integer, message: 'admin.market.non_integer_position' },
values: { value: -> (p){ p >= ::Market::TOP_POSITION }, message: 'admin.market.invalid_position' },
desc: -> { API::V2::Admin::Entities::Market.documentation[:position][:desc] }
optional :min_price,
type: { value: BigDecimal, message: 'admin.market.non_decimal_min_price' },
values: { value: -> (p){ p >= 0 }, message: 'admin.market.invalid_min_price' },
desc: -> { API::V2::Admin::Entities::Market.documentation[:min_price][:desc] }
optional :min_amount,
type: { value: BigDecimal, message: 'admin.market.non_decimal_min_amount' },
values: { value: -> (p){ p >= 0 }, message: 'admin.market.invalid_min_amount' },
desc: -> { API::V2::Admin::Entities::Market.documentation[:min_amount][:desc] }
end
post '/markets/update' do
admin_authorize! :update, ::Market
market = ::Market.find(params[:id])
if market.update(declared(params, include_missing: false))
present market, with: API::V2::Admin::Entities::Market
else
body errors: market.errors.full_messages
status 422
end
end
end
end
end
end

View File

@@ -0,0 +1,90 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
class Members < Grape::API
helpers ::API::V2::Admin::Helpers
desc 'Get all members, result is paginated.',
is_array: true,
success: API::V2::Admin::Entities::Member
params do
optional :state,
desc: 'Filter order by state.'
optional :role,
values: { value: -> { ::Ability.roles }, message: 'admin.member.invalid_role' }
optional :group,
values: { value: -> { ::Member.groups }, message: 'admin.member.invalid_group' }
optional :email,
desc: -> { API::V2::Entities::Member.documentation[:email][:desc] }
use :uid
use :date_picker
use :pagination
use :ordering
end
get '/members' do
admin_authorize! :read, Member
ransack_params = Helpers::RansackBuilder.new(params)
.eq(:uid, :email, :state, :role, :group)
.with_daterange
.build
search = Member.ransack(ransack_params)
search.sorts = "#{params[:order_by]} #{params[:ordering]}"
present paginate(search.result), with: API::V2::Admin::Entities::Member
end
desc 'Get available members groups.',
is_array: true
get '/members/groups' do
admin_authorize! :read, Member
Member.groups
end
desc 'Get a member.' do
success API::V2::Admin::Entities::Member
end
params do
requires :uid,
type: String,
desc: 'The shared user ID.'
end
get '/members/:uid' do
admin_authorize! :read, Member
present Member.find_by!(uid: params[:uid]), with: API::V2::Admin::Entities::Member
end
desc 'Set user group.' do
success API::V2::Admin::Entities::Member
end
params do
requires :uid,
type: String,
desc: 'The shared user ID.'
requires :group,
type: String,
coerce_with: ->(v) { v.strip.downcase },
desc: 'User gruop'
end
put '/members/:uid' do
admin_authorize! :update, Member
declared_params = declared(params)
member = Member.find_by!(uid: declared_params[:uid])
if member.update(group: declared_params[:group])
present member, with: API::V2::Admin::Entities::Member
status 201
else
body errors: member.errors.full_messages
status 422
end
end
end
end
end
end

67
app/api/v2/admin/mount.rb Normal file
View File

@@ -0,0 +1,67 @@
# frozen_string_literal: true
module API
module V2
module Admin
class Mount < Grape::API
PREFIX = '/admin'
before { authenticate! unless request.path == '/api/v2/admin/swagger' }
formatter :csv, CSVFormatter
mount Admin::Orders
mount Admin::Blockchains
mount Admin::Currencies
mount Admin::Markets
mount Admin::Wallets
mount Admin::Deposits
mount Admin::Withdraws
mount Admin::Trades
mount Admin::Operations
mount Admin::Members
mount Admin::TradingFees
mount Admin::Adjustments
mount Admin::Engines
mount Admin::Beneficiaries
mount Admin::Abilities
mount Admin::WithdrawLimits
mount Admin::DepositLimits
mount Admin::Airdrops
mount Admin::InternalTransfers
mount Admin::WhitelistedSmartContracts
# The documentation is accessible at http://localhost:3000/swagger?url=/api/v2/admin/swagger
# Add swagger documentation for Peatio Admin API
add_swagger_documentation base_path: File.join(API::Mount::PREFIX, API::V2::Mount::API_VERSION, PREFIX, 'peatio'),
add_base_path: true,
mount_path: '/swagger',
api_version: API::V2::Mount::API_VERSION,
doc_version: Peatio::Application::VERSION,
info: {
title: "Peatio Admin API #{API::V2::Mount::API_VERSION}",
description: 'Admin API high privileged API with RBAC.',
contact_name: 'openware.com',
contact_email: 'hello@openware.com',
contact_url: 'https://www.openware.com',
licence: 'MIT',
license_url: 'https://github.com/openware/peatio/blob/master/LICENSE.md'
},
models: [
API::V2::Admin::Entities::Blockchain,
API::V2::Admin::Entities::Currency,
API::V2::Admin::Entities::Deposit,
API::V2::Admin::Entities::Market,
API::V2::Admin::Entities::Member,
API::V2::Admin::Entities::Operation,
API::V2::Admin::Entities::Order,
API::V2::Admin::Entities::Trade,
API::V2::Admin::Entities::Wallet,
API::V2::Admin::Entities::Withdraw,
API::V2::Admin::Entities::InternalTransfer,
API::V2::Admin::Entities::WhitelistedSmartContract
]
end
end
end
end

View File

@@ -0,0 +1,84 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
class Operations < Grape::API
helpers ::API::V2::Admin::Helpers
helpers do
params :get_operations_params do
optional :reference_type,
desc: 'The reference type for which operation was created.'
optional :rid,
type: Integer,
desc: 'The unique id of operation\'s reference, for which operation was created.'
optional :code,
type: Integer,
desc: 'Opeartion\'s code.'
use :currency
use :date_picker
end
def ransack_params
Helpers::RansackBuilder.new(params)
.eq(:code, :reference_type)
.translate(currency: :currency_id, rid: :reference_id)
.with_daterange
.build
end
end
# GET: api/v2/admin/assets
# GET: api/v2/admin/expenses
# GET: api/v2/admin/revenues
::Operations::Account::PLATFORM_TYPES.each do |op_type|
op_type_plural = op_type.to_s.pluralize
desc "Returns #{op_type_plural} as a paginated collection." do
success API::V2::Admin::Entities::Operation
end
params do
use :get_operations_params
use :pagination
use :ordering
end
get op_type_plural do
klass = ::Operations.const_get(op_type.capitalize)
admin_authorize! :read, klass
search = klass.ransack(ransack_params)
search.sorts = "#{params[:order_by]} #{params[:ordering]}"
present paginate(search.result, false), with: API::V2::Admin::Entities::Operation
end
end
# Get: api/v2/admin/liabilities
::Operations::Account::MEMBER_TYPES.each do |op_type|
op_type_plural = op_type.to_s.pluralize
desc "Returns #{op_type_plural} as a paginated collection." do
success API::V2::Admin::Entities::Operation
end
params do
use :uid
use :get_operations_params
use :pagination
use :ordering
end
get op_type_plural do
klass = ::Operations.const_get(op_type.capitalize)
admin_authorize! :read, klass
member = Member.find_by(uid: params[:uid]) if params[:uid].present?
search = klass.ransack(ransack_params.merge(member_id_eq: member&.id))
search.sorts = "#{params[:order_by]} #{params[:ordering]}"
present paginate(search.result, false), with: API::V2::Admin::Entities::Operation
end
end
end
end
end
end

122
app/api/v2/admin/orders.rb Normal file
View File

@@ -0,0 +1,122 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
class Orders < Grape::API
helpers ::API::V2::Admin::Helpers
helpers ::API::V2::OrderHelpers
content_type :csv, 'text/csv'
desc 'Get all orders, result is paginated.',
is_array: true,
success: API::V2::Admin::Entities::Order
params do
optional :market,
values: { value: -> { ::Market.ids }, message: 'admin.market.doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Market.documentation[:id][:desc] }
optional :state,
values: { value: -> { ::Order.state.values }, message: 'admin.order.invalid_state' },
desc: 'Filter order by state.'
optional :ord_type,
values: { value: ::Order::TYPES, message: 'admin.order.invalid_ord_type' },
desc: 'Filter order by ord_type.'
optional :price,
type: { value: BigDecimal, message: 'admin.order.non_decimal_price' },
values: { value: -> (p){ p.try(:positive?) }, message: 'admin.order.non_positive_price' },
desc: -> { API::V2::Admin::Entities::Order.documentation[:price][:desc] }
optional :origin_volume,
type: { value: BigDecimal, message: 'admin.order.non_decimal_price' },
values: { value: -> (p){ p.try(:positive?) }, message: 'admin.order.non_positive_origin_volume' },
desc: -> { API::V2::Admin::Entities::Order.documentation[:origin_volume][:desc] }
optional :type,
values: { value: %w(sell buy), message: 'admin.order.invalid_type' },
desc: 'Filter order by type.'
optional :email,
desc: -> { API::V2::Entities::Member.documentation[:email][:desc] }
use :uid
use :date_picker
use :pagination
use :ordering
end
get '/orders' do
admin_authorize! :read, Order
if params[:uid].present? || params[:email].present?
member = Member.find_by('uid = ? OR email = ?', params[:uid], params[:email])
params.except!(:uid, :email).merge!(member_id: member.id) if member.present?
end
ransack_params = Helpers::RansackBuilder.new(params)
.eq(:price, :origin_volume, :ord_type, :state, :member_id)
.translate(market: :market_id)
.with_daterange
.merge({
type_eq: params[:type].present? ? params[:type] == 'buy' ? 'OrderBid' : 'OrderAsk' : nil
}).build
search = Order.ransack(ransack_params)
search.sorts = "#{params[:order_by]} #{params[:ordering]}"
if params[:format] == 'csv'
search.result
else
present paginate(search.result, false), with: API::V2::Admin::Entities::Order
end
end
desc 'Cancel an order.'
params do
requires :id,
type: { value: Integer, message: 'admin.order.non_integer_id' },
allow_blank: false,
desc: -> { API::V2::Admin::Entities::Order.documentation[:id][:desc] }
end
post '/orders/:id/cancel' do
admin_authorize! :update, ::Order
begin
order = Order.find(params[:id])
order.trigger_cancellation
present order, with: API::V2::Admin::Entities::Order
rescue ActiveRecord::RecordNotFound => e
# RecordNotFound in rescued by ExceptionsHandler.
raise(e)
rescue
error!({ errors: ['admin.order.cancel_error'] }, 422)
end
end
desc 'Cancel all orders.'
params do
requires :market,
values: { value: -> { ::Market.active.ids }, message: 'admin.order.market_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Order.documentation[:id][:desc] }
optional :side,
values: { value: %w(sell buy), message: 'admin.order.invalid_side' },
desc: 'If present, only sell orders (asks) or buy orders (bids) will be cancelled.'
end
post '/orders/cancel' do
admin_authorize! :update, ::Order
begin
ransack_params = Helpers::RansackBuilder.new(params)
.eq(state: 'wait')
.translate(market: :market_id)
.merge({
type_eq: params[:side].present? ? params[:side] == 'buy' ? 'OrderBid' : 'OrderAsk' : nil,
}).build
orders = Order.ransack(ransack_params)
orders.result.map(&:trigger_cancellation)
present orders.result, with: API::V2::Entities::Order
rescue
error!({ errors: ['admin.order.cancel_error'] }, 422)
end
end
end
end
end
end

View File

@@ -0,0 +1,68 @@
# encoding: UTF-8
# frozen_string_literal: true
require 'csv'
module API
module V2
module Admin
class Trades < Grape::API
helpers ::API::V2::Admin::Helpers
content_type :csv, 'text/csv'
desc 'Get all trades, result is paginated.',
is_array: true,
success: API::V2::Admin::Entities::Trade
params do
optional :market,
values: { value: -> { ::Market.ids }, message: 'admin.market.doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Market.documentation[:id][:desc] }
optional :order_id,
type: Integer,
desc: -> { API::V2::Entities::Order.documentation[:id][:desc] }
use :uid
use :date_picker
use :pagination
use :ordering
end
get '/trades' do
admin_authorize! :read, Trade
member = Member.find_by(uid: params[:uid]) if params[:uid].present?
ransack_params = Helpers::RansackBuilder.new(params.except!(:uid))
.translate(market: :market_id)
.with_daterange
.merge(g: [
{ maker_id_eq: member&.id, taker_id_eq: member&.id, m: 'or' },
{ maker_order_id_eq: params[:order_id], taker_order_id_eq: params[:order_id], m: 'or' },
]).build
search = Trade.ransack(ransack_params)
search.sorts = "#{params[:order_by]} #{params[:ordering]}"
if params[:format] == 'csv'
search.result
else
present paginate(search.result, false), with: API::V2::Admin::Entities::Trade
end
end
desc 'Get a trade with detailed information.' do
success API::V2::Admin::Entities::Blockchain
end
params do
requires :id,
type: { value: Integer, message: 'admin.trade.non_integer_id' },
desc: -> { API::V2::Admin::Entities::Trade.documentation[:id][:desc] }
end
get '/trades/:id' do
admin_authorize! :read, Trade
present Trade.find(params[:id]), with: API::V2::Admin::Entities::Trade, extended: true
end
end
end
end
end

View File

@@ -0,0 +1,125 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
class TradingFees < Grape::API
helpers ::API::V2::Admin::Helpers
desc 'Returns trading_fees table as paginated collection',
is_array: true,
success: API::V2::Entities::TradingFee
params do
optional :group,
type: String,
desc: -> { API::V2::Entities::TradingFee.documentation[:group][:desc] },
coerce_with: ->(c) { c.strip.downcase }
optional :market_id,
type: String,
desc: -> { API::V2::Entities::TradingFee.documentation[:market_id][:desc] },
values: { value: -> { ::Market.ids.append(::TradingFee::ANY) },
message: 'admin.trading_fee.market_doesnt_exist' }
use :pagination
use :ordering
end
get '/trading_fees' do
admin_authorize! :read, TradingFee
ransack_params = Helpers::RansackBuilder.new(params)
.eq(:group, :market_id)
.build
search = TradingFee.ransack(ransack_params)
search.sorts = "#{params[:order_by]} #{params[:ordering]}"
present paginate(search.result), with: API::V2::Entities::TradingFee
end
desc 'It creates trading fees record',
success: API::V2::Entities::TradingFee
params do
requires :maker,
type: { value: BigDecimal, message: 'admin.trading_fee.non_decimal_maker' },
values: { value: -> (p){ p && p >= 0 }, message: 'admin.trading_fee.invalid_maker' },
desc: -> { API::V2::Entities::TradingFee.documentation[:maker][:desc] }
requires :taker,
type: { value: BigDecimal, message: 'admin.trading_fee.non_decimal_taker' },
values: { value: -> (p){ p && p >= 0 }, message: 'admin.trading_fee.invalid_taker' },
desc: -> { API::V2::Entities::TradingFee.documentation[:taker][:desc] }
optional :group,
type: String,
default: ::TradingFee::ANY,
desc: -> { API::V2::Entities::TradingFee.documentation[:group][:desc] }
optional :market_id,
type: String,
desc: -> { API::V2::Entities::TradingFee.documentation[:market_id][:desc] },
default: ::TradingFee::ANY,
values: { value: -> { ::Market.ids.append(::TradingFee::ANY) },
message: 'admin.trading_fee.market_doesnt_exist' }
end
post '/trading_fees/new' do
admin_authorize! :create, TradingFee
trading_fee = ::TradingFee.new(declared(params))
if trading_fee.save
present trading_fee, with: API::V2::Entities::TradingFee
status 201
else
body errors: trading_fee.errors.full_messages
status 422
end
end
desc 'It updates trading fees record',
success: API::V2::Entities::TradingFee
params do
requires :id,
type: { value: Integer, message: 'admin.trading_fee.non_integer_id' },
desc: -> { API::V2::Entities::TradingFee.documentation[:id][:desc] }
optional :maker,
type: { value: BigDecimal, message: 'admin.trading_fee.non_decimal_maker' },
values: { value: -> (p){ p && p >= 0 }, message: 'admin.trading_fee.invalid_maker' },
desc: -> { API::V2::Entities::TradingFee.documentation[:maker][:desc] }
optional :taker,
type: { value: BigDecimal, message: 'admin.trading_fee.non_decimal_taker' },
values: { value: -> (p){ p && p >= 0 }, message: 'admin.trading_fee.invalid_taker' },
desc: -> { API::V2::Entities::TradingFee.documentation[:taker][:desc] }
optional :group,
type: String,
coerce_with: ->(c) { c.strip.downcase },
desc: -> { API::V2::Entities::TradingFee.documentation[:group][:desc] }
optional :market_id,
type: String,
desc: -> { API::V2::Entities::TradingFee.documentation[:market_id][:desc] },
values: { value: -> { ::Market.ids.append(::TradingFee::ANY) },
message: 'admin.trading_fee.market_doesnt_exist' }
end
post '/trading_fees/update' do
admin_authorize! :update, TradingFee
trading_fee = ::TradingFee.find(params[:id])
if trading_fee.update(declared(params, include_missing: false))
present trading_fee, with: API::V2::Entities::TradingFee
else
body errors: trading_fee.errors.full_messages
status 422
end
end
desc 'It deletes trading fees record',
success: API::V2::Entities::TradingFee
params do
requires :id,
type: { value: Integer, message: 'admin.trading_fee.non_integer_id' },
desc: -> { API::V2::Entities::TradingFee.documentation[:id][:desc] }
end
post '/trading_fees/delete' do
admin_authorize! :delete, TradingFee
present TradingFee.destroy(params[:id]), with: API::V2::Entities::TradingFee
end
end
end
end
end

249
app/api/v2/admin/wallets.rb Normal file
View File

@@ -0,0 +1,249 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Admin
class Wallets < Grape::API
helpers ::API::V2::Admin::Helpers
helpers do
# Collection of shared params, used to
# generate required/optional Grape params.
OPTIONAL_WALLET_PARAMS ||= {
max_balance: {
type: { value: BigDecimal, message: 'admin.blockchain.non_decimal_max_balance' },
values: { value: -> (p){ p >= 0 }, message: 'admin.wallet.invalid_max_balance' },
default: 0.0,
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:max_balance][:desc] }
},
status: {
values: { value: %w(active disabled), message: 'admin.wallet.invalid_status' },
default: 'active',
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:status][:desc] }
},
}
params :create_wallet_params do
OPTIONAL_WALLET_PARAMS.each do |key, params|
optional key, params
end
end
params :update_wallet_params do
OPTIONAL_WALLET_PARAMS.each do |key, params|
optional key, params.except(:default)
end
end
end
desc 'Get all wallets, result is paginated.',
is_array: true,
success: API::V2::Admin::Entities::Wallet
params do
optional :blockchain_key,
values: { value: -> { ::Blockchain.pluck(:key) }, message: 'admin.currency.blockchain_key_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:blockchain_key][:desc] }
optional :kind,
values: { value: -> { Wallet.kind.values }, message: 'admin.wallet.invalid_kind' },
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:kind][:desc] }
optional :currencies,
values: { value: ->(v) { (Array.wrap(v) - ::Currency.codes).blank? }, message: 'admin.wallet.currency_doesnt_exist' },
types: [String, Array], coerce_with: ->(c) { Array.wrap(c) },
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:currencies][:desc] }
use :pagination
use :ordering
end
get '/wallets' do
admin_authorize! :read, Wallet
ransack_params = Helpers::RansackBuilder.new(params)
.eq(:blockchain_key)
.translate_in(currencies: :currencies_id)
.merge(kind_eq: params[:kind].present? ? Wallet.kinds[params[:kind].to_sym] : nil)
.build
search = ::Wallet.ransack(ransack_params)
search.sorts = "#{params[:order_by]} #{params[:ordering]}"
present paginate(search.result.includes(:currencies).distinct), with: API::V2::Admin::Entities::Wallet
end
desc 'List wallet kinds.'
get '/wallets/kinds' do
::Wallet.kind.values
end
desc 'List wallet gateways.'
get '/wallets/gateways' do
::Wallet.gateways.map(&:to_s)
end
desc 'Get a wallet.' do
success API::V2::Admin::Entities::Wallet
end
params do
requires :id,
type: { value: Integer, message: 'admin.wallet.non_integer_id' },
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:id][:desc] }
end
get '/wallets/:id' do
admin_authorize! :read, Wallet
present ::Wallet.find(params[:id]), with: API::V2::Admin::Entities::Wallet
end
desc 'Creates new wallet.' do
success API::V2::Admin::Entities::Wallet
end
params do
use :create_wallet_params
requires :blockchain_key,
values: { value: -> { ::Blockchain.pluck(:key) }, message: 'admin.wallet.blockchain_key_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:blockchain_key][:desc] }
requires :name,
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:name][:desc] }
optional :address,
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:address][:desc] }
optional :currencies,
values: { value: ->(v) { (Array.wrap(v) - ::Currency.codes).blank? }, message: 'admin.wallet.currency_doesnt_exist' },
types: [String, Array], coerce_with: ->(c) { Array.wrap(c) },
as: :currency_ids,
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:currencies][:desc] }
# @deprecated Please use `currencies` field
optional :currency,
values: { value: -> { ::Currency.codes }, message: 'admin.wallet.currency_doesnt_exist' },
as: :currency_ids,
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:currencies][:desc] }
requires :kind,
values: { value: ::Wallet.kind.values, message: 'admin.wallet.invalid_kind' },
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:kind][:desc] }
requires :gateway,
values: { value: -> { ::Wallet.gateways.map(&:to_s) }, message: 'admin.wallet.gateway_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:gateway][:desc] }
optional :settings, type: JSON,
default: {},
desc: -> { 'Wallet settings (uri, secret)' } do
optional :uri,
values: { value: ->(v) { URI.parse(v).is_a?(URI::HTTP) || URI.parse(v).is_a?(URI::HTTPS) }, message: 'admin.wallet.invalid_uri_setting' },
desc: -> { 'Wallet uri setting' }
optional :secret,
desc: -> { 'Wallet secret setting' }
end
exactly_one_of :currencies, :currency, message: 'admin.wallet.currencies_field_is_missing'
end
post '/wallets/new' do
admin_authorize! :create, Wallet
wallet = ::Wallet.new(declared(params))
if wallet.save
present wallet, with: API::V2::Admin::Entities::Wallet
status 201
else
body errors: wallet.errors.full_messages
status 422
end
end
desc 'Update wallet.' do
success API::V2::Admin::Entities::Wallet
end
params do
use :update_wallet_params
requires :id,
type: { value: Integer, message: 'admin.wallet.non_integer_id' },
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:id][:desc] }
optional :blockchain_key,
values: { value: -> { ::Blockchain.pluck(:key) }, message: 'admin.wallet.blockchain_key_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:blockchain_key][:desc] }
optional :name,
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:name][:desc] }
optional :address,
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:address][:desc] }
optional :kind,
values: { value: ::Wallet.kind.values, message: 'admin.wallet.invalid_kind' },
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:kind][:desc] }
optional :gateway,
values: { value: -> { ::Wallet.gateways.map(&:to_s) }, message: 'admin.wallet.gateway_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:gateway][:desc] }
optional :currencies,
values: { value: ->(v) { (Array.wrap(v) - ::Currency.codes).blank? }, message: 'admin.wallet.currency_doesnt_exist' },
types: [String, Array], coerce_with: ->(c) { Array.wrap(c) },
as: :currency_ids,
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:currencies][:desc] }
optional :settings, type: JSON,
desc: -> { 'Wallet settings' } do
optional :uri,
values: { value: ->(v) { URI.parse(v).is_a?(URI::HTTP) || URI.parse(v).is_a?(URI::HTTPS) }, message: 'admin.wallet.invalid_uri_setting' },
desc: -> { 'Wallet uri setting' }
optional :secret,
desc: -> { 'Wallet secret setting' }
end
end
post '/wallets/update' do
admin_authorize! :update, Wallet
wallet = ::Wallet.find(params[:id])
declared_params = declared(params, include_missing: false)
declared_params.merge!(settings: params[:settings]) if params[:settings].present?
if wallet.update(declared_params)
present wallet, with: API::V2::Admin::Entities::Wallet
else
body errors: wallet.errors.full_messages
status 422
end
end
desc 'Add currency to the wallet' do
success API::V2::Admin::Entities::Wallet
end
params do
requires :id,
type: { value: Integer, message: 'admin.wallet.non_integer_id' },
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:id][:desc] }
requires :currencies,
values: { value: ->(v) { (Array.wrap(v) - ::Currency.codes).blank? }, message: 'admin.wallet.currency_doesnt_exist' },
types: [String, Array], coerce_with: ->(c) { Array.wrap(c) },
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:currencies][:desc] }
end
post '/wallets/currencies' do
wallet = Wallet.find(params[:id])
wallet.transaction do
params[:currencies].each do |c_id|
c_w = CurrencyWallet.new(currency_id: c_id, wallet_id: params[:id])
error!({ errors: c_w.errors.full_messages }, 422) unless c_w.save
end
end
present wallet, with: API::V2::Admin::Entities::Wallet
status 201
end
desc 'Delete currency from the wallet' do
success API::V2::Admin::Entities::Wallet
end
params do
requires :id,
type: { value: Integer, message: 'admin.wallet.non_integer_id' },
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:id][:desc] }
requires :currencies,
values: { value: ->(v) { (Array.wrap(v) - ::Currency.codes).blank? }, message: 'admin.wallet.currency_doesnt_exist' },
types: [String, Array], coerce_with: ->(c) { Array.wrap(c) },
desc: -> { API::V2::Admin::Entities::Wallet.documentation[:currencies][:desc] }
end
delete '/wallets/currencies' do
wallet = Wallet.find(params[:id])
wallet.transaction do
params[:currencies].each do |c_id|
# Check if exist (will return error)
CurrencyWallet.find_by!(currency_id: c_id, wallet_id: params[:id])
# Delete relation
CurrencyWallet.where(currency_id: c_id, wallet_id: params[:id]).delete_all
end
end
present wallet, with: API::V2::Admin::Entities::Wallet
end
end
end
end
end

View File

@@ -0,0 +1,137 @@
# frozen_string_literal: true
module API
module V2
module Admin
class WhitelistedSmartContracts < Grape::API
helpers ::API::V2::Admin::Helpers
content_type :csv, 'text/csv'
desc 'Get all whitelisted addresses, result is paginated.',
is_array: true,
success: API::V2::Admin::Entities::WhitelistedSmartContract
params do
optional :blockchain_key,
values: { value: -> { ::Blockchain.pluck(:key) }, message: 'admin.whitelistedsmartcontract.blockchain_key_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::WhitelistedSmartContract.documentation[:blockchain_key][:desc] }
use :pagination
use :ordering
end
get '/whitelisted_smart_contracts' do
admin_authorize! :read, WhitelistedSmartContract
ransack_params = Helpers::RansackBuilder.new(params).eq(:blockchain_key).build
search = ::WhitelistedSmartContract.ransack(ransack_params)
search.sorts = "#{params[:order_by]} #{params[:ordering]}"
if params[:format] == 'csv'
search.result
else
present paginate(search.result), with: API::V2::Admin::Entities::WhitelistedSmartContract
end
end
desc 'Get a whitelisted address.' do
success API::V2::Admin::Entities::WhitelistedSmartContract
end
params do
requires :id,
type: { value: Integer, message: 'admin.whitelistedsmartcontract.non_integer_id' },
desc: -> { API::V2::Admin::Entities::WhitelistedSmartContract.documentation[:id][:desc] }
end
get '/whitelisted_smart_contract/:id' do
admin_authorize! :read, WhitelistedSmartContract
present ::WhitelistedSmartContract.find(params[:id]), with: API::V2::Admin::Entities::WhitelistedSmartContract
end
desc 'Creates new whitelisted address.' do
success API::V2::Admin::Entities::WhitelistedSmartContract
end
params do
requires :blockchain_key,
values: { value: -> { ::Blockchain.pluck(:key) }, message: 'admin.whitelistedsmartcontract.blockchain_key_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::WhitelistedSmartContract.documentation[:blockchain_key][:desc] }
requires :address,
desc: -> { API::V2::Admin::Entities::WhitelistedSmartContract.documentation[:address][:desc] }
optional :description,
desc: -> { API::V2::Admin::Entities::WhitelistedSmartContract.documentation[:description][:desc] }
optional :state,
values: { value: %w[active disabled], message: 'admin.whitelistedsmartcontract.invalid_state' },
default: 'active',
desc: -> { API::V2::Admin::Entities::WhitelistedSmartContract.documentation[:state][:desc] }
end
post '/whitelisted_smart_contracts' do
admin_authorize! :create, WhitelistedSmartContract
whitelisted_address = ::WhitelistedSmartContract.new(declared(params))
if whitelisted_address.save
present whitelisted_address, with: API::V2::Admin::Entities::WhitelistedSmartContract
else
body errors: whitelisted_address.errors.full_messages
status 422
end
end
desc 'Update whitelisted_smart_contract.' do
success API::V2::Admin::Entities::WhitelistedSmartContract
end
params do
requires :id,
type: { value: Integer, message: 'admin.whitelistedsmartcontract.non_integer_id' },
desc: -> { API::V2::Admin::Entities::WhitelistedSmartContract.documentation[:id][:desc] }
optional :blockchain_key,
values: { value: -> { ::Blockchain.pluck(:key) }, message: 'admin.whitelistedsmartcontract.blockchain_key_doesnt_exist' },
desc: -> { API::V2::Admin::Entities::WhitelistedSmartContract.documentation[:blockchain_key][:desc] }
optional :description,
desc: -> { API::V2::Admin::Entities::WhitelistedSmartContract.documentation[:description][:desc] }
optional :address,
desc: -> { API::V2::Admin::Entities::WhitelistedSmartContract.documentation[:address][:desc] }
optional :state,
values: { value: %w[active disabled], message: 'admin.whitelistedsmartcontract.invalid_state' },
desc: -> { API::V2::Admin::Entities::WhitelistedSmartContract.documentation[:state][:desc] }
end
put '/whitelisted_smart_contracts' do
admin_authorize! :update, WhitelistedSmartContract
whitelisted_address = ::WhitelistedSmartContract.find(params[:id])
declared_params = declared(params, include_missing: false)
if whitelisted_address.update(declared_params)
present whitelisted_address, with: API::V2::Admin::Entities::WhitelistedSmartContract
else
body errors: whitelisted_address.errors.full_messages
status 422
end
end
desc 'Process whitelisted smart contracts from csv' do
success API::V2::Admin::Entities::WhitelistedSmartContract
end
params do
requires :file,
type: File,
desc: -> {'CSV file with whitelisted smart contracts data'}
end
post '/whitelisted_smart_contracts/csv' do
admin_authorize! :create, WhitelistedSmartContract
count = 0
CSV.parse(params[:file][:tempfile], headers: true, quote_empty: false).each do |row|
row = row.to_h.compact.symbolize_keys!
address = row[:address]
blockchain_key = row[:blockchain_key]
description = row[:description]
next if address.blank? || blockchain_key.blank? || ::Blockchain.pluck(:key).exclude?(blockchain_key)
::WhitelistedSmartContract.create!(description: description, address: address,
blockchain_key: blockchain_key, state: 'active')
count += 1
end
present ::WhitelistedSmartContract.last(count), with: API::V2::Admin::Entities::WhitelistedSmartContract
end
end
end
end
end

View File

@@ -0,0 +1,127 @@
# frozen_string_literal: true
module API
module V2
module Admin
class WithdrawLimits < Grape::API
helpers ::API::V2::Admin::Helpers
desc 'Returns withdraw limits table as paginated collection',
is_array: true,
success: API::V2::Admin::Entities::WithdrawLimit
params do
optional :group,
type: String,
desc: -> { API::V2::Entities::WithdrawLimit.documentation[:group][:desc] },
coerce_with: ->(c) { c.strip.downcase }
optional :kyc_level,
type: String,
desc: -> { API::V2::Entities::WithdrawLimit.documentation[:kyc_level][:desc] }
optional :kind,
values: { value: ->(v) { (Array.wrap(v) - WithdrawLimit.kind.values).blank? }, message: 'limit.kind.invalid' },
desc: -> { API::V2::Entities::WithdrawLimit.documentation[:kind][:desc] }
use :pagination
use :ordering
end
get '/withdraw_limits' do
admin_authorize! :read, WithdrawLimit
ransack_params = Helpers::RansackBuilder.new(params)
.eq(:group, :kyc_level, :kind)
.build
search = WithdrawLimit.ransack(ransack_params)
search.sorts = "#{params[:order_by]} #{params[:ordering]}"
present paginate(search.result), with: API::V2::Entities::WithdrawLimit
end
desc 'It creates withdraw limits record',
success: API::V2::Entities::WithdrawLimit
params do
requires :limit_24_hour,
type: { value: BigDecimal, message: 'admin.withdraw_limit.non_decimal_limit_24_hour' },
values: { value: ->(p) { p && p >= 0 }, message: 'admin.withdraw_limit.invalid_limit_24_hour' },
desc: -> { API::V2::Entities::WithdrawLimit.documentation[:limit_24_hour][:desc] }
requires :limit_1_month,
type: { value: BigDecimal, message: 'admin.withdraw_limit.non_decimal_limit_1_month' },
values: { value: ->(p) { p && p >= 0 }, message: 'admin.withdraw_limit.invalid_limit_1_month' },
desc: -> { API::V2::Entities::WithdrawLimit.documentation[:limit_1_month][:desc] }
requires :kind,
values: { value: ->(v) { (Array.wrap(v) - WithdrawLimit.kind.values).blank? }, message: 'limit.kind.invalid' },
desc: -> { API::V2::Entities::WithdrawLimit.documentation[:kind][:desc] }
optional :group,
type: String,
default: ::WithdrawLimit::ANY,
desc: -> { API::V2::Entities::WithdrawLimit.documentation[:group][:desc] }
optional :kyc_level,
type: String,
default: ::WithdrawLimit::ANY,
desc: -> { API::V2::Entities::WithdrawLimit.documentation[:kyc_level][:desc] }
end
post '/withdraw_limits' do
admin_authorize! :create, WithdrawLimit
withdraw_limit = ::WithdrawLimit.new(declared(params))
if withdraw_limit.save
present withdraw_limit, with: API::V2::Entities::WithdrawLimit
status 201
else
body errors: withdraw_limit.errors.full_messages
status 422
end
end
desc 'It updates withdraw limits record',
success: API::V2::Entities::WithdrawLimit
params do
requires :id,
type: { value: Integer, message: 'admin.withdraw_limit.non_integer_id' },
desc: -> { API::V2::Entities::WithdrawLimit.documentation[:id][:desc] }
optional :limit_24_hour,
type: { value: BigDecimal, message: 'admin.withdraw_limit.non_decimal_limit_24_hour' },
values: { value: ->(p) { p && p >= 0 }, message: 'admin.withdraw_limit.invalid_limit_24_hour' },
desc: -> { API::V2::Entities::WithdrawLimit.documentation[:limit_24_hour][:desc] }
optional :limit_1_month,
type: { value: BigDecimal, message: 'admin.withdraw_limit.non_decimal_limit_1_month' },
values: { value: ->(p) { p && p >= 0 }, message: 'admin.withdraw_limit.invalid_limit_1_month' },
desc: -> { API::V2::Entities::WithdrawLimit.documentation[:limit_1_month][:desc] }
optional :kind,
values: { value: ->(v) { (Array.wrap(v) - WithdrawLimit.kind.values).blank? }, message: 'limit.kind.invalid' },
desc: -> { API::V2::Entities::WithdrawLimit.documentation[:kind][:desc] }
optional :kyc_level,
type: String,
desc: -> { API::V2::Entities::WithdrawLimit.documentation[:kyc_level][:desc] }
optional :group,
type: String,
coerce_with: ->(c) { c.strip.downcase },
desc: -> { API::V2::Entities::WithdrawLimit.documentation[:group][:desc] }
end
put '/withdraw_limits' do
admin_authorize! :update, WithdrawLimit
withdraw_limit = ::WithdrawLimit.find(params[:id])
if withdraw_limit.update(declared(params, include_missing: false))
present withdraw_limit, with: API::V2::Entities::WithdrawLimit
else
body errors: withdraw_limit.errors.full_messages
status 422
end
end
desc 'It deletes withdraw limits record',
success: API::V2::Entities::WithdrawLimit
params do
requires :id,
type: { value: Integer, message: 'admin.withdraw_limit.non_integer_id' },
desc: -> { API::V2::Entities::WithdrawLimit.documentation[:id][:desc] }
end
delete '/withdraw_limits/:id' do
admin_authorize! :delete, WithdrawLimit
present WithdrawLimit.destroy(params[:id]), with: API::V2::Entities::WithdrawLimit
end
end
end
end
end

View File

@@ -0,0 +1,149 @@
# frozen_string_literal: true
module API
module V2
module Admin
class Withdraws < Grape::API
helpers ::API::V2::Admin::Helpers
desc 'Get all withdraws, result is paginated.',
is_array: true,
success: API::V2::Admin::Entities::Withdraw
params do
optional :state,
values: { value: ->(v) { (Array.wrap(v) - Withdraw::STATES.map(&:to_s)).blank? }, message: 'admin.withdraw.invalid_state' },
desc: -> { API::V2::Admin::Entities::Withdraw.documentation[:state][:desc] }
optional :id,
type: Integer,
desc: -> { API::V2::Admin::Entities::Withdraw.documentation[:id][:desc] }
optional :txid,
desc: -> { API::V2::Admin::Entities::Withdraw.documentation[:blockchain_txid][:desc] }
optional :tid,
desc: -> { API::V2::Admin::Entities::Withdraw.documentation[:tid][:desc] }
optional :confirmations,
type: Integer,
desc: -> { API::V2::Admin::Entities::Withdraw.documentation[:confirmations][:desc] }
optional :rid,
desc: -> { API::V2::Admin::Entities::Withdraw.documentation[:rid][:desc] }
optional :wallet_type,
values: { value: ->(v) { (Array.wrap(v.to_sym) - Wallet.gateways).blank? }, message: 'admin.withdraw.invalid_wallet_type' },
desc: -> { 'Select withdraw that can be processed from wallets with given type e.g. patiry' }
use :uid
use :currency
use :currency_type
use :date_picker
use :pagination
use :ordering
end
get '/withdraws' do
admin_authorize! :read, Withdraw
ransack_params = Helpers::RansackBuilder.new(params)
.eq(:id, :txid, :rid, :tid)
.translate(uid: :member_uid, currency: :currency_id)
.with_daterange
.merge(type_eq: params[:type].present? ? "Withdraws::#{params[:type].capitalize}" : nil)
.merge(aasm_state_in: params[:state])
.build
search = Withdraw.ransack(ransack_params)
search.sorts = "#{params[:order_by]} #{params[:ordering]}"
if params[:wallet_type].present?
present paginate(search.result
.where(currency: Currency.joins(:wallets)
.where(wallets: { id: Wallet.where(gateway: params[:wallet_type]) }))),
with: API::V2::Admin::Entities::Withdraw
else
present paginate(search.result), with: API::V2::Admin::Entities::Withdraw
end
end
desc 'Get withdraw by ID.',
success: API::V2::Admin::Entities::Withdraw
params do
requires :id,
type: { value: Integer, message: 'admin.withdraw.non_integer_id' },
desc: -> { API::V2::Admin::Entities::Withdraw.documentation[:id][:desc] }
end
get '/withdraws/:id' do
admin_authorize! :read, Withdraw
withdraw = Withdraw.find_by!(id: params[:id])
present withdraw,
with: API::V2::Admin::Entities::Withdraw,
with_beneficiary: true
end
desc 'Take an action on the withdrawal.',
success: API::V2::Admin::Entities::Withdraw
params do
requires :id,
type: Integer,
desc: -> { API::V2::Admin::Entities::Withdraw.documentation[:id][:desc] }
requires :action,
type: String,
values: { value: -> { ::Withdraw.aasm.events.map(&:name).map(&:to_s) }, message: 'admin.withdraw.invalid_action' },
desc: "Valid actions are #{::Withdraw.aasm.events.map(&:name)}."
given action: ->(action) { %w[load dispatch success].include?(action) } do
optional :txid,
type: String,
desc: -> { API::V2::Admin::Entities::Withdraw.documentation[:blockchain_txid][:desc] }
end
end
post '/withdraws/actions' do
admin_authorize! :update, Withdraw
declared_params = declared(params, include_missing: false)
withdraw = Withdraw.find(declared_params[:id])
if withdraw.currency.fiat? && declared_params[:txid].present?
error!({ errors: ['admin.withdraw.redundant_txid'] }, 422)
end
transited = withdraw.transaction do
withdraw.update!(txid: declared_params[:txid]) if declared_params[:txid].present?
withdraw.public_send("#{declared_params[:action]}!").tap do |success|
raise ActiveRecord::Rollback unless success
end
rescue StandardError
raise ActiveRecord::Rollback
end
if transited
present withdraw, with: API::V2::Admin::Entities::Withdraw
else
body errors: ["admin.withdraw.cannot_#{declared_params[:action]}"]
status 422
end
end
desc 'Update withdraw request',
success: API::V2::Admin::Entities::Withdraw
params do
requires :id,
type: Integer,
desc: -> { API::V2::Admin::Entities::Withdraw.documentation[:id][:desc] }
optional :metadata,
type: JSON,
desc: 'Optional metadata to be applied to the transaction.'
end
put '/withdraws' do
admin_authorize! :update, Withdraw
declared_params = declared(params, include_missing: false)
withdraw = Withdraw.find(declared_params[:id])
declared_params[:metadata] = withdraw.metadata.merge(declared_params[:metadata]) if declared_params[:metadata].present?
if withdraw.update(declared_params)
present withdraw, with: API::V2::Admin::Entities::Withdraw
else
body errors: withdraw.errors.full_messages
status 422
end
end
end
end
end
end

17
app/api/v2/auth/utils.rb Normal file
View File

@@ -0,0 +1,17 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Auth
module Utils
class << self
def cache
# Simply use rack-attack cache wrapper
@cache ||= Rack::Attack::Cache.new
end
end
end
end
end
end

View File

@@ -0,0 +1,49 @@
# frozen_string_literal: true
module API
module V2
module CoinGecko
module Entities
class Orderbook < API::V2::Entities::Base
expose(
:ticker_id,
documentation: {
type: String,
desc: 'A pair such as "BTC_ETH", with delimiter between different cryptoassets.'
}
) do |orderbook|
orderbook.market.underscore_name
end
expose(
:timestamp,
documentation: {
type: Integer,
desc: 'Unix timestamp in milliseconds for when the last updated time occurred'
}
) do
DateTime.now.strftime('%Q').to_i
end
expose(
:asks,
documentation: {
type: BigDecimal,
is_array: true,
desc: 'An array containing 2 elements. The offer price and quantity for each bid order.'
}
)
expose(
:bids,
documentation: {
type: BigDecimal,
is_array: true,
desc: 'An array containing 2 elements. The ask price and quantity for each ask order.'
}
)
end
end
end
end
end

View File

@@ -0,0 +1,41 @@
# frozen_string_literal: true
module API
module V2
module CoinGecko
module Entities
class Pair < API::V2::Entities::Base
expose(
:ticker_id,
documentation: {
type: String,
desc: 'Identifier of a ticker with delimiter to separate base/target, eg. BTC_ETH.'
}
) do |market|
market.underscore_name
end
expose(
:base,
documentation: {
type: String,
desc: 'Symbol/currency code of a the base cryptoasset, eg. BTC.'
}
) do |market|
market[:base_unit].upcase
end
expose(
:target,
documentation: {
type: String,
desc: 'Symbol/currency code of the target cryptoasset, eg. ETH.'
}
) do |market|
market[:quote_unit].upcase
end
end
end
end
end
end

View File

@@ -0,0 +1,111 @@
# frozen_string_literal: true
module API
module V2
module CoinGecko
module Entities
class Ticker < API::V2::Entities::Base
expose(
:ticker_id,
documentation: {
type: String,
desc: 'Identifier of a ticker with delimiter to separate base/target, eg. BTC_ETH.'
}
) do |ticker|
ticker[:market].underscore_name
end
expose(
:base_currency,
documentation: {
type: String,
desc: 'Symbol/currency code of base pair, eg. BTC.'
}
) do |ticker|
ticker[:market][:base_unit].upcase
end
expose(
:target_currency,
documentation: {
type: String,
desc: 'Symbol/currency code of target pair, eg. ETH.'
}
) do |ticker|
ticker[:market][:quote_unit].upcase
end
expose(
:last_price,
documentation: {
type: BigDecimal,
desc: 'Last transacted price of base currency based on given target currency.'
}
) do |ticker|
ticker[:last]
end
expose(
:base_volume,
documentation: {
type: BigDecimal,
desc: '24 hour trading volume in base pair volume.'
}
) do |ticker|
ticker[:amount]
end
expose(
:target_volume,
documentation: {
type: BigDecimal,
desc: '24 hour trading volume in base pair volume.'
}
) do |ticker|
ticker[:volume]
end
expose(
:bid,
documentation: {
type: BigDecimal,
desc: 'Current highest bid price.'
}
) do |ticker|
OrderBid.get_depth(ticker[:market].id).flatten.first.to_d
end
expose(
:ask,
documentation: {
type: BigDecimal,
desc: 'Current lowest ask price.'
}
) do |ticker|
OrderAsk.get_depth(ticker[:market].id).flatten.first.to_d
end
expose(
:high,
documentation: {
type: BigDecimal,
desc: 'The highest trade price during last 24 hours (0.0 if no trades executed during last 24 hours).'
}
) do |ticker|
ticker[:high]
end
expose(
:low,
documentation: {
type: BigDecimal,
desc: 'The lowest trade price during last 24 hours (0.0 if no trades executed during last 24 hours).'
}
) do |ticker|
ticker[:low]
end
end
end
end
end
end

View File

@@ -0,0 +1,22 @@
# frozen_string_literal: true
module API
module V2
module CoinGecko
module Helpers
MILLISECONDS_IN_SECOND = 1000
def format_trade(trade)
{
trade_id: trade[:id],
price: trade[:price],
base_volume: trade[:amount],
target_volume: trade[:total],
trade_timestamp: trade[:created_at] * MILLISECONDS_IN_SECOND,
type: trade[:taker_type]
}
end
end
end
end
end

View File

@@ -0,0 +1,43 @@
# frozen_string_literal: true
module API
module V2
module CoinGecko
class HistoricalTrades < Grape::API
desc 'Get recent trades on market'
params do
requires :ticker_id,
type: String,
desc: 'A pair such as "LTC_BTC"',
coerce_with: ->(name) { name.strip.split('_').join.downcase }
optional :type,
type: String,
values: { value: %w(buy sell), message: 'coingecko.historical_trades.invalid_type' },
desc: 'To indicate nature of trade - buy/sell'
optional :limit,
type: Integer,
values: { value: 0..1000, message: 'coingecko.historical_trades.invalid_limit' },
desc: 'Number of historical trades to retrieve from time of query. [0, 200, 500...]. 0 returns full history'
optional :start_time,
type: Integer,
desc: '',
coerce_with: ->(start_time) { Time.parse(start_time).to_i }
optional :end_time,
type: Integer,
desc: '',
coerce_with: ->(end_time) { Time.parse(end_time).to_i }
end
get '/historical_trades' do
market = ::Market.find(params[:ticker_id])
filters = declared(params, include_missing: false)
.except(:ticker_id, :limit)
Trade.public_from_influx(market.id, params[:limit], filters).each_with_object({'buy' => [], 'sell' => []}) do |trade, hash|
hash[trade[:taker_type]] << format_trade(trade)
end
end
end
end
end
end

View File

@@ -0,0 +1,20 @@
# frozen_string_literal: true
module API
module V2
module CoinGecko
class Mount < Grape::API
PREFIX = '/coingecko'
before { set_ets_context! }
helpers CoinGecko::Helpers
mount CoinGecko::Pairs
mount CoinGecko::Tickers
mount CoinGecko::Orderbook
mount CoinGecko::HistoricalTrades
end
end
end
end

View File

@@ -0,0 +1,39 @@
# frozen_string_literal: true
module API
module V2
module CoinGecko
class Orderbook < Grape::API
class OrderBook < Struct.new(:market, :asks, :bids); end
desc 'Get depth or specified market'
params do
requires :ticker_id,
type: String,
desc: 'A pair such as "LTC_BTC"',
coerce_with: ->(name) { name.strip.split('_').join.downcase }
optional :depth,
type: { value: Integer, message: 'coingecko.market_depth.non_integer_depth' },
values: { value: 0..1000, message: 'coingecko.market_depth.invalid_depth' },
desc: 'Orders depth quantity: [0, 100, 200, 500...]'
end
get '/orderbook' do
market = ::Market.find(params[:ticker_id])
asks = OrderAsk.get_depth(market.id)
bids = OrderBid.get_depth(market.id)
# Depth = 100 means 50 for each bid/ask side
# Not defined or 0 = full order book
unless params[:depth].to_d.zero?
asks = asks[0, params[:depth]/2]
bids = bids[0, params[:depth]/2]
end
orderbook = OrderBook.new market, asks, bids
present orderbook, with: API::V2::CoinGecko::Entities::Orderbook
end
end
end
end
end

View File

@@ -0,0 +1,16 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module CoinGecko
class Pairs < Grape::API
desc 'Get list of all available trading pairs'
get "/pairs" do
present ::Rails.cache.fetch(:markets_coingecko, expires_in: 60) { ::Market.enabled.ordered },
with: API::V2::CoinGecko::Entities::Pair
end
end
end
end
end

View File

@@ -0,0 +1,21 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module CoinGecko
class Tickers < Grape::API
desc 'Get list of all available trading pairs'
get "/tickers" do
tickers = ::Rails.cache.fetch(:markets_tickers_coingecko, expires_in: 60) do
::Market.enabled.ordered.inject([]) do |hash, market|
hash << TickersService[market].ticker.merge(market: market)
end
end
present tickers, with: API::V2::CoinGecko::Entities::Ticker
end
end
end
end
end

View File

@@ -0,0 +1,16 @@
# frozen_string_literal: true
module API
module V2
module CoinMarketCap
class Assets < Grape::API
desc 'Details on crypto currencies available on the exchange'
get '/assets' do
::Rails.cache.fetch(:currencies_cmc, expires_in: 600) do
format_currencies(Currency.visible.coins.ordered.map)
end
end
end
end
end
end

View File

@@ -0,0 +1,93 @@
# frozen_string_literal: true
module API
module V2
module CoinMarketCap
module Helpers
MILLISECONDS_IN_SECOND = 1000
def format_summary(ticker, market)
lowest_ask = OrderAsk.get_depth(market.id)
highest_bid = OrderBid.get_depth(market.id)
{
trading_pairs: market.underscore_name, # mandatory [string]: Identifier of a ticker with delimiter to separate base/quote
base_currency: market.base_unit.upcase, # recommended [string]: Symbol/currency code of base currency
quote_currency: market.quote_unit.upcase, # recommended [string]: Symbol/currency code of base currency
last_price: ticker[:last], # mandatory [decimal]: Last transacted price of base currency based on given quote currency
lowest_ask: lowest_ask.flatten.first.to_d, # mandatory [decimal]: Lowest Ask price of base currency based on given quote currency
highest_bid: highest_bid.flatten.first.to_d, # mandatory [decimal]: Highest bid price of base currency based on given quote currency
base_volume: ticker[:amount], # mandatory [decimal]: 24-hr volume of market pair denoted in BASE currency
quote_volume: ticker[:volume], # mandatory [decimal]: 24-hr volume of market pair denoted in QUOTE currency
price_change_percent_24h: price_change_percent_24h(ticker), # mandatory [decimal]: 24-hr % price change of market pair
highest_price_24h: ticker[:high], # mandatory [decimal]: Highest price of base currency based on given quote currency in the last 24-hrs
lowest_price_24h: ticker[:low] # mandatory [decimal]: Lowest price of base currency based on given quote currency in the last 24-hrs
}
end
def format_tickers(markets)
markets.each_with_object({}) do |market, h|
ticker = TickersService[market].ticker
unified_base_crypto_id = market.base.coin? ? unified_cryptoasset_id(market.base_unit) : nil
unified_quote_crypto_id = market.quote.coin? ? unified_cryptoasset_id(market.quote_unit) : nil
h[market.underscore_name.to_s] = {
base_id: unified_base_crypto_id, # recommended [integer]: The quote pair Unified Cryptoasset ID
quote_id: unified_quote_crypto_id, # recommended [integer]: The base pair Unified Cryptoasset ID
last_price: ticker[:last], # mandatory [decimal]: Last transacted price of base currency based on given quote currenc
base_volume: ticker[:amount], # mandatory [decimal]: 24-hour trading volume denoted in BASE currency
quote_volume: ticker[:volume], # mandatory [decimal]: 24 hour trading volume denoted in QUOTE currency
isFrozen: market.state == 'enabled' ? 0 : 1 # recommended [integer]: Indicates if the market is currently enabled (0) or disabled (1)
}.compact
end
end
def format_trade(trade)
{
trade_id: trade[:id], # mandatory [integer]: A unique ID associated with the trade for the currency pair transaction
price: trade[:price], # mandatory [decimal]: Last transacted price of base currency based on given quote currency
base_volume: trade[:amount], # mandatory [decimal]: Transaction amount in BASE currency
quote_volume: trade[:total], # mandatory [decimal]: Transaction amount in QUOTE currency
timestamp: trade[:created_at] * MILLISECONDS_IN_SECOND, # mandatory [integer]: Unix timestamp in milliseconds for when the transaction occurred
type: trade[:taker_type] # mandatory [string]: Used to determine whether or not the transaction originated as a buy or sell
}
end
def format_orderbook(asks, bids)
{
timestamp: DateTime.now.strftime('%Q').to_i, # mandotory [decimal]: Unix timestamp in milliseconds for when the last updated time occurred
asks: asks, # mandotory [decimal]: The offer price and quantity for each bid order
bids: bids # mandotory [decimal]: The ask price and quantity for each ask order.
}
end
def format_currencies(currencies)
currencies.each_with_object({}) do |currency, h|
h[currency.id.upcase.to_s] = {
name: currency.name, # recommended [string]: Full name of cryptocurrency
unified_cryptoasset_id: unified_cryptoasset_id(currency.id), # recommended [integer]: Unique ID of cryptocurrency assigned by Unified Cryptoasset ID
can_withdraw: currency.withdrawal_enabled, # recommended [boolean]: Identifies whether withdrawals are enabled or disabled
can_deposit: currency.deposit_enabled, # recommended [boolean]: Identifies whether deposits are enabled or disabled
min_withdraw: currency.min_withdraw_amount # recommended [decimal]: Identifies the single minimum withdrawal amount of a cryptocurrency
}.compact
end
end
# Only for crypto currencies
def unified_cryptoasset_id(currency_id)
# System will get response in such format [{:id=>1,:name=>"Bitcoin",:symbol=>"BTC"}]
::CoinMarketCap.default_client.get(symbol: currency_id)[0][:id]
rescue ::Faraday::Error, ::CoinMarketCap::Error => e
Rails.logger.warn e
# In format methods we skipped all nil values with compact method
# As unified_cryptoasset_id is only recommended field
nil
end
# System use this methods instead of ticker[:price_change_percent]
# because we dont need percentage symbols here
def price_change_percent_24h(ticker)
ticker[:open].to_d.zero? ? '0.0' : (ticker[:last].to_d - ticker[:open].to_d) / ticker[:open].to_d
end
end
end
end
end

View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true
module API
module V2
module CoinMarketCap
class Mount < Grape::API
before { set_ets_context! }
helpers CoinMarketCap::Helpers
mount CoinMarketCap::Summary
mount CoinMarketCap::Assets
mount CoinMarketCap::Ticker
mount CoinMarketCap::Trades
mount CoinMarketCap::Orderbook
end
end
end
end

View File

@@ -0,0 +1,35 @@
# frozen_string_literal: true
module API
module V2
module CoinMarketCap
class Orderbook < Grape::API
desc 'Get depth or specified market'
params do
requires :market_pair,
type: String,
desc: 'A pair such as "LTC_BTC"',
coerce_with: ->(name) { name.strip.split('_').join.downcase }
optional :depth,
type: { value: Integer, message: 'coinmarketcap.market_depth.non_integer_depth' },
values: { value: 0..500, message: 'coinmarketcap.market_depth.invalid_depth' },
desc: 'Orders depth quantity: [0,5,10,20,50,100,500]'
end
get "/orderbook/:market_pair" do
market = ::Market.find(params[:market_pair])
asks = OrderAsk.get_depth(market.id)
bids = OrderBid.get_depth(market.id)
# Depth = 100 means 50 for each bid/ask side
# Not defined or 0 = full order book
unless params[:depth].to_d.zero?
asks = asks[0, params[:depth]/2]
bids = bids[0, params[:depth]/2]
end
format_orderbook(asks, bids)
end
end
end
end
end

View File

@@ -0,0 +1,18 @@
# frozen_string_literal: true
module API
module V2
module CoinMarketCap
class Summary < Grape::API
desc 'Overview of market data for all tickers and all market pairs on the exchange'
get '/summary' do
::Rails.cache.fetch(:markets_summary_cmc, expires_in: 60) do
::Market.enabled.ordered.map do |market|
format_summary(TickersService[market].ticker, market)
end
end
end
end
end
end
end

View File

@@ -0,0 +1,16 @@
# frozen_string_literal: true
module API
module V2
module CoinMarketCap
class Ticker < Grape::API
desc 'Get 24-hour pricing and volume summary for each market pair'
get '/ticker' do
::Rails.cache.fetch(:markets_tickers_cmc, expires_in: 60) do
format_tickers(::Market.enabled.ordered)
end
end
end
end
end
end

View File

@@ -0,0 +1,21 @@
# frozen_string_literal: true
module API
module V2
module CoinMarketCap
class Trades < Grape::API
desc 'Get recent trades on market'
params do
requires :market_pair,
type: String,
desc: 'A pair such as "LTC_BTC"',
coerce_with: ->(name) { name.strip.split('_').join.downcase }
end
get "/trades/:market_pair" do
market = ::Market.find(params[:market_pair])
Trade.public_from_influx(market.id).map { |trade| format_trade(trade) }
end
end
end
end
end

22
app/api/v2/constraints.rb Normal file
View File

@@ -0,0 +1,22 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Constraints
class << self
def included(base)
rate_limit = ENV.fetch("PEATIO_RATE_LIMIT_5MIN", 6000).to_i
apply_rules!(rate_limit)
base.use Rack::Attack
end
def apply_rules!(rate_limit)
Rack::Attack.throttle 'Limit number of calls to API', limit: rate_limit, period: 5.minutes do |req|
req.env['api_v2.authentic_member_email']
end
end
end
end
end
end

View File

@@ -0,0 +1,50 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Entities
class Account < Base
expose(
:currency_id,
as: :currency,
documentation: {
desc: 'Currency code.',
type: String
}
)
expose(
:balance,
format_with: :decimal,
documentation: {
desc: 'Account balance.',
type: BigDecimal
}
)
expose(
:locked,
format_with: :decimal,
documentation: {
desc: 'Account locked funds.',
type: BigDecimal
}
)
expose(
:deposit_address,
if: ->(account, _options) { account.currency.coin? },
using: API::V2::Entities::PaymentAddress,
documentation: {
desc: 'User deposit address',
type: String
}
) do |account, options|
wallet = Wallet.deposit_wallet(account.currency_id)
::PaymentAddress.find_by(wallet: wallet, member: options[:current_user], remote: false)
end
end
end
end
end

View File

@@ -0,0 +1,13 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Entities
class Base < Grape::Entity
format_with(:iso8601) {|t| t.to_time.in_time_zone(::Rails.configuration.time_zone).iso8601 if t }
format_with(:decimal) {|d| d.to_s('F') if d }
end
end
end
end

View File

@@ -0,0 +1,88 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Entities
class Beneficiary < Base
expose(
:id,
documentation: {
desc: 'Beneficiary Identifier in Database',
type: Integer
}
)
expose(
:currency_id,
as: :currency,
documentation: {
desc: 'Beneficiary currency code.',
type: String
}
)
expose(
:uid,
documentation: {
desc: 'Beneficiary owner',
type: String
}
) { |b| b.member.uid }
expose(
:name,
documentation: {
desc: 'Human rememberable name which refer beneficiary.',
type: String
}
)
expose(
:description,
documentation: {
desc: 'Human rememberable description of beneficiary.',
type: String
}
)
# expose(
# :data,
# documentation: {
# desc: 'Bank Account details for fiat Beneficiary in JSON format.'\
# 'For crypto it\'s blockchain address.',
# type: JSON
# }
# ) do |beneficiary|
# beneficiary.currency.fiat? ? beneficiary.masked_data : beneficiary.data
# end
expose(
:data,
documentation: {
desc: 'Bank Account details for fiat Beneficiary in JSON format.'\
'For crypto it\'s blockchain address.',
type: JSON
}
)
expose(
:state,
documentation: {
desc: 'Defines either beneficiary active - user can use it to withdraw money'\
'or pending - requires beneficiary activation with pin.',
type: String
}
)
expose(
:sent_at,
format_with: :iso8601,
documentation: {
desc: 'Time when last pin was sent',
type: String
}
)
end
end
end
end

View File

@@ -0,0 +1,38 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Entities
class Bonus < Base
expose(
:bonus_member_id,
as: :uid,
documentation: {
desc: 'user_uid.',
type: String
}
)
expose(
:amount,
format_with: :decimal,
documentation: {
desc: 'Bonus account',
type: BigDecimal
}
)
expose(
:created_at,
as: :date,
format_with: :iso8601,
documentation: {
desc: 'created date',
type: Date
}
)
end
end
end
end

View File

@@ -0,0 +1,188 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Entities
class Currency < Base
expose(
:id,
documentation: {
desc: 'Currency code.',
type: String,
values: -> { ::Currency.visible.codes },
example: -> { ::Currency.visible.first.id }
}
)
expose(
:name,
documentation: {
type: String,
desc: 'Currency name',
example: -> { ::Currency.visible.first.name }
},
if: -> (currency){ currency.name.present? }
)
expose(
:description,
documentation: {
type: String,
desc: 'Currency description',
example: -> { ::Currency.visible.first.id }
}
)
expose(
:homepage,
documentation: {
type: String,
desc: 'Currency homepage',
example: -> { ::Currency.visible.first.id }
}
)
expose(
:price,
documentation: {
desc: 'Currency current price'
}
)
expose(
:explorer_transaction,
documentation: {
desc: 'Currency transaction exprorer url template',
example: 'https://testnet.blockchain.info/tx/'
},
if: -> (currency){ currency.coin? }
)
expose(
:explorer_address,
documentation: {
desc: 'Currency address exprorer url template',
example: 'https://testnet.blockchain.info/address/'
},
if: -> (currency){ currency.coin? }
)
expose(
:type,
documentation: {
type: String,
values: -> { ::Currency.types },
desc: 'Currency type',
example: -> { ::Currency.visible.first.type }
}
)
expose(
:deposit_enabled,
documentation: {
type: String,
desc: 'Currency deposit possibility status (true/false).'
}
)
expose(
:withdrawal_enabled,
documentation: {
type: String,
desc: 'Currency withdrawal possibility status (true/false).'
}
)
expose(
:deposit_fee,
documentation: {
desc: 'Currency deposit fee',
example: -> { ::Currency.visible.first.deposit_fee }
}
)
expose(
:min_deposit_amount,
documentation: {
desc: 'Minimal deposit amount',
example: -> { ::Currency.visible.first.min_deposit_amount }
}
)
expose(
:withdraw_fee,
documentation: {
desc: 'Currency withdraw fee',
example: -> { ::Currency.visible.first.withdraw_fee }
}
)
expose(
:min_withdraw_amount,
documentation: {
desc: 'Minimal withdraw amount',
example: -> { ::Currency.visible.first.min_withdraw_amount }
}
)
expose(
:withdraw_limit_24h,
documentation: {
desc: 'Currency 24h withdraw limit',
example: -> { ::Currency.visible.first.withdraw_limit_24h }
}
)
expose(
:withdraw_limit_72h,
documentation: {
desc: 'Currency 72h withdraw limit',
example: -> { ::Currency.visible.first.withdraw_limit_72h }
}
)
expose(
:base_factor,
documentation: {
desc: 'Currency base factor',
example: -> { ::Currency.visible.first.base_factor }
}
)
expose(
:precision,
documentation: {
desc: 'Currency precision',
example: -> { ::Currency.visible.first.precision }
}
)
expose(
:position,
documentation: {
desc: 'Position used for defining currencies order',
example: -> { ::Currency.visible.first.precision }
}
)
expose(
:icon_url,
documentation: {
desc: 'Currency icon',
example: 'https://upload.wikimedia.org/wikipedia/commons/0/05/Ethereum_logo_2014.svg'
},
if: -> (currency){ currency.icon_url.present? }
)
expose(
:min_confirmations,
if: ->(currency) { currency.coin? },
documentation: {
desc: 'Number of confirmations required for confirming deposit or withdrawal'
}
) { |c| c.blockchain.min_confirmations }
end
end
end
end

View File

@@ -0,0 +1,104 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Entities
class Deposit < Base
expose(
:id,
documentation: {
type: Integer,
desc: 'Unique deposit id.'
}
)
expose(
:currency_id,
as: :currency,
documentation: {
type: String,
desc: 'Deposit currency id.'
}
)
expose(
:amount,
format_with: :decimal,
documentation: {
type: BigDecimal,
desc: 'Deposit amount.'
}
)
expose(
:fee,
documentation: {
type: BigDecimal,
desc: 'Deposit fee.'
}
)
expose(
:txid,
documentation: {
type: String,
desc: 'Deposit transaction id.'
}
)
expose(
:confirmations,
documentation: {
type: Integer,
desc: 'Number of deposit confirmations.'
},
if: ->(deposit) { deposit.currency.coin? }
)
expose(
:aasm_state,
as: :state,
documentation: {
type: String,
desc: 'Deposit state.'
}
)
expose(
:transfer_type,
documentation: {
type: String,
desc: 'Deposit transfer type'
}
)
expose(
:created_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'The datetime when deposit was created.'
}
)
expose(
:completed_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'The datetime when deposit was completed..'
}
)
expose(
:tid,
documentation: {
type: String,
desc: 'The shared transaction ID'
}
)
end
end
end
end

View File

@@ -0,0 +1,73 @@
module API
module V2
module Entities
class DepositLimit < API::V2::Entities::Base
expose(
:id,
documentation:{
type: Integer,
desc: 'Unique deposit limit table identifier in database.'
}
)
expose(
:group,
documentation:{
type: String,
desc: 'Member group for define deposit limits.'
}
)
expose(
:kyc_level,
documentation:{
type: String,
desc: 'KYC level for define deposit limits.'
}
)
expose(
:kind,
documentation:{
type: String,
desc: 'kind for define deposit limits.'
}
)
expose(
:limit_24_hour,
documentation:{
type: BigDecimal,
desc: '24 hours deposit limit.'
}
)
expose(
:limit_1_month,
documentation:{
type: BigDecimal,
desc: '1 month deposit limit.'
}
)
expose(
:created_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'deposit limit table created time in iso8601 format.'
}
)
expose(
:updated_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'deposit limit table updated time in iso8601 format.'
}
)
end
end
end
end

View File

@@ -0,0 +1,98 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Entities
class InternalTransfer < Base
expose(
:currency_id,
as: :currency,
documentation: {
type: String,
desc: 'The currency code.'
}
)
expose(
:sender_username,
documentation: {
type: String,
desc: 'The internal transfer sender.'
}
) do |transfer|
transfer.sender&.username
end
expose(
:receiver_username,
documentation: {
type: String,
desc: 'The internal transfer receiver.'
}
) do |transfer|
transfer.receiver&.username
end
expose(
:sender_uid,
documentation: {
type: String,
desc: 'The internal transfer sender.'
}
) do |transfer|
transfer.sender.uid
end
expose(
:receiver_uid,
documentation: {
type: String,
desc: 'The internal transfer receiver.'
}
) do |transfer|
transfer.receiver.uid
end
expose(
:direction, ## call method from model
documentation: {
type: String,
desc: 'The internal transfer direction (incoming or outcoming internal transfer).'
}
) do |transfer, options|
transfer.direction(options[:current_user])
end
expose(
:amount,
format_with: :decimal,
documentation: {
type: BigDecimal,
desc: 'Internal transfer Amount.'
}
)
expose(
:state,
as: :status,
documentation: {
type: String,
desc: 'The internal transfer state.'
}
)
expose(
:created_at,
:updated_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'The datetimes for the internal transfer.'
}
)
end
end
end
end

View File

@@ -0,0 +1,93 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Entities
class Market < Base
expose(
:id,
documentation: {
type: String,
desc: "Unique market id. It's always in the form of xxxyyy,"\
"where xxx is the base currency code, yyy is the quote"\
"currency code, e.g. 'btcusd'. All available markets can"\
"be found at /api/v2/markets."
}
)
expose(
:name,
documentation: {
type: String,
desc: 'Market name.'
}
)
expose(
:base_unit,
documentation: {
type: String,
desc: "Market Base unit."
}
)
expose(
:quote_unit,
documentation: {
type: String,
desc: "Market Quote unit."
}
)
expose(
:min_price,
documentation: {
type: BigDecimal,
desc: "Minimum order price."
}
)
expose(
:max_price,
documentation: {
type: BigDecimal,
desc: "Maximum order price."
}
)
expose(
:min_amount,
documentation: {
type: BigDecimal,
desc: "Minimum order amount."
}
)
expose(
:amount_precision,
documentation: {
type: BigDecimal,
desc: "Precision for order amount."
}
)
expose(
:price_precision,
documentation: {
type: BigDecimal,
desc: "Precision for order price."
}
)
expose(
:state,
documentation: {
type: String,
desc: "Market state defines if user can see/trade on current market."
}
)
end
end
end
end

View File

@@ -0,0 +1,38 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Entities
class Member < Base
expose(
:uid,
documentation: {
type: String,
desc: 'Member UID.'
}
)
expose(
:email,
documentation: {
type: String,
desc: 'Member email.'
}
)
expose(
:accounts,
using: API::V2::Entities::Account,
documentation: {
type: 'API::V2::Entities::Account',
is_array: true,
desc: 'Member accounts.'
}
) do |m|
m.accounts.includes(:currency)
end
end
end
end
end

View File

@@ -0,0 +1,166 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Entities
class Order < Base
expose(
:id,
documentation:{
type: Integer,
desc: "Unique order id."
}
)
expose(
:uuid,
documentation:{
type: String,
desc: "Unique order UUID."
}
)
expose(
:side,
documentation: {
type: String,
desc: "Either 'sell' or 'buy'."
}
)
expose(
:ord_type,
documentation: {
type: String,
desc: "Type of order, either 'limit' or 'market'."
}
)
expose(
:price,
documentation: {
type: BigDecimal,
desc: "Price for each unit. e.g."\
"If you want to sell/buy 1 btc at 3000 usd, the price is '3000.0'"
}
)
expose(
:avg_price,
documentation: {
type: BigDecimal,
desc: "Average execution price, average of price in trades."
}
)
expose(
:state,
documentation: {
type: String,
desc: "One of 'wait', 'done', or 'cancel'."\
"An order in 'wait' is an active order, waiting fulfillment;"\
"a 'done' order is an order fulfilled;"\
"'cancel' means the order has been canceled."
}
)
expose(
:market_id,
as: :market,
documentation: {
type: String,
desc: "The market in which the order is placed, e.g. 'btcusd'."\
"All available markets can be found at /api/v2/markets."
}
)
expose(
:created_at,
format_with: :iso8601,
documentation: {
type: String,
desc: "Order create time in iso8601 format."
}
)
expose(
:updated_at,
format_with: :iso8601,
documentation: {
type: String,
desc: "Order updated time in iso8601 format."
}
)
expose(
:origin_volume,
documentation: {
type: BigDecimal,
desc: "The amount user want to sell/buy."\
"An order could be partially executed,"\
"e.g. an order sell 5 btc can be matched with a buy 3 btc order,"\
"left 2 btc to be sold; in this case the order's volume would be '5.0',"\
"its remaining_volume would be '2.0', its executed volume is '3.0'."
}
)
expose(
:volume,
as: :remaining_volume,
documentation: {
type: BigDecimal,
desc: "The remaining volume, see 'volume'."
}
)
expose(
:executed_volume,
documentation: {
type: BigDecimal,
desc: "The executed volume, see 'volume'."
}
) do |order, _options|
order.origin_volume - order.volume
end
expose(
:maker_fee,
documentation: {
type: BigDecimal,
desc: "Fee for maker."
}
)
expose(
:taker_fee,
documentation: {
type: BigDecimal,
desc: "Fee for taker."
}
)
expose(
:trades_count,
documentation: {
type: Integer,
desc: "Count of trades."
}
)
expose(
:trades,
documentation: {
type: 'API::V2::Entities::Trade',
is_array: true,
desc: "Trades wiht this order."
},
if: { type: :full }
) do |order, _options|
API::V2::Entities::Trade.represent order.trades, side: order.side
end
end
end
end
end

View File

@@ -0,0 +1,32 @@
# encoding: UTF-8
# frozen_string_literal: true
require_dependency 'v2/entities/order'
module API
module V2
module Entities
class OrderBook < Base
expose(
:asks,
using: Order,
documentation: {
type: 'API::V2::Entities::Order',
is_array: true,
desc: 'Asks in orderbook'
}
)
expose(
:bids,
using: Order,
documentation: {
type: 'API::V2::Entities::Order',
is_array: true,
desc: 'Bids in orderbook'
}
)
end
end
end
end

View File

@@ -0,0 +1,41 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Entities
class PaymentAddress < Base
expose(
:currencies,
documentation: {
desc: 'Currencies codes.',
is_array: true,
example: -> { ::Currency.visible.codes }
}
) do |pa|
pa.wallet.currencies.codes
end
expose(
:address,
documentation: {
desc: 'Payment address.',
type: String
}
) do |pa, options|
options[:address_format] ? pa.format_address(options[:address_format]) : pa.address
end
expose(
:state,
documentation: {
desc: 'Payment address state.',
type: String
}
) do |pa|
pa.address.present? ? 'active' : 'pending'
end
end
end
end
end

View File

@@ -0,0 +1,91 @@
# frozen_string_literal: true
module API
module V2
module Entities
class Pnl < Base
expose(
:currency_id,
as: :currency,
documentation: {
type: String,
desc: 'Currency code.'
}
)
expose(
:pnl_currency_id,
as: :pnl_currency,
documentation: {
type: String,
desc: 'PnL currency code.'
}
)
expose(
:total_credit,
documentation: {
type: BigDecimal,
desc: 'Total credit amount.'
}
)
expose(
:total_debit,
documentation: {
type: BigDecimal,
desc: 'Total debit amount.'
}
)
expose(
:total_credit_value,
documentation: {
type: BigDecimal,
desc: 'Total credit value in pnl currency.'
}
)
expose(
:total_debit_value,
documentation: {
type: BigDecimal,
desc: 'Total debit value in pnl currency.'
}
)
expose(
:average_buy_price,
documentation: {
type: BigDecimal,
desc: 'Average buy price.'
}
)
expose(
:average_sell_price,
documentation: {
type: BigDecimal,
desc: 'Average sell price.'
}
)
expose(
:average_balance_price,
documentation: {
type: BigDecimal,
desc: 'Average balance price.'
}
)
expose(
:total_balance_value,
documentation: {
type: BigDecimal,
desc: 'Total balance value in pnl currency.'
}
)
end
end
end
end

View File

@@ -0,0 +1,66 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Entities
class PublicTrade < Base
expose(
:id,
documentation: {
type: String,
desc: 'Trade ID.'
}
)
expose(
:price,
documentation: {
type: BigDecimal,
desc: 'Trade price.'
}
)
expose(
:amount,
documentation: {
type: BigDecimal,
desc: 'Trade amount.'
}
)
expose(
:total,
documentation: {
type: BigDecimal,
desc: 'Trade total (Amount * Price).'
}
)
expose(
:market,
documentation: {
type: String,
desc: 'Trade market id.'
}
)
expose(
:created_at,
documentation: {
type: String,
desc: 'Trade create time in iso8601 format.'
}
)
expose(
:taker_type,
documentation: {
type: String,
desc: 'Trade taker order type (sell or buy).'
}
)
end
end
end
end

View File

@@ -0,0 +1,111 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Entities
class Ticker < Base
class TickerEntry < Base
expose(
:low,
documentation: {
type: BigDecimal,
desc: 'The lowest trade price during last 24 hours (0.0 if no trades executed during last 24 hours)'
}
)
expose(
:high,
documentation: {
type: BigDecimal,
desc: 'The highest trade price during last 24 hours (0.0 if no trades executed during last 24 hours)'
}
)
expose(
:open,
documentation: {
type: BigDecimal,
desc: 'Price of the first trade executed 24 hours ago or less'
}
)
expose(
:last,
documentation: {
type: BigDecimal,
desc: 'The last executed trade price'
}
)
expose(
:volume,
documentation: {
type: BigDecimal,
desc: 'Total volume of trades executed during last 24 hours'
}
)
expose(
:amount,
documentation: {
type: BigDecimal,
desc: 'Total amount of trades executed during last 24 hours'
}
)
expose(
:vol,
documentation: {
type: BigDecimal,
desc: 'Alias to volume'
}
)
expose(
:avg_price,
documentation: {
type: BigDecimal,
desc: 'Average price more precisely VWAP is calculated by adding up the total traded for every transaction'\
'(price multiplied by the number of shares traded) and then dividing by the total shares traded'
}
)
expose(
:price_change_percent,
documentation: {
type: String,
desc: 'Price change in the next format +3.19%.'\
'Price change is calculated using next formula (last - open) / open * 100%'
}
)
expose(
:at,
documentation: {
type: Integer,
desc: 'Timestamp of ticker'
}
)
end
expose(
:at,
documentation: {
type: Integer,
desc: 'Timestamp of ticker'
}
)
expose(
:ticker,
using: TickerEntry,
documentation: {
type: TickerEntry,
desc: 'Ticker entry for specified time'
}
)
end
end
end
end

View File

@@ -0,0 +1,133 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Entities
class Trade < Base
expose(
:id,
documentation: {
type: String,
desc: 'Trade ID.'
}
)
expose(
:price,
documentation: {
type: BigDecimal,
desc: 'Trade price.'
}
)
expose(
:amount,
documentation: {
type: BigDecimal,
desc: 'Trade amount.'
}
)
expose(
:total,
documentation: {
type: BigDecimal,
desc: 'Trade total (Amount * Price).'
}
)
expose(
:fee_currency,
documentation: {
type: BigDecimal,
desc: 'Currency user\'s fees were charged in.'
},
if: ->(_, options) { options[:current_user] }
) do |trade, options|
fee_currency(trade.order_for_member(options[:current_user]))
end
expose(
:fee,
documentation: {
type: BigDecimal,
desc: 'Percentage of fee user was charged for performed trade.'
},
if: ->(_, options) { options[:current_user] }
) do |trade, options|
trade.order_fee(trade.order_for_member(options[:current_user]))
end
expose(
:fee_amount,
documentation: {
type: BigDecimal,
desc: 'Amount of fee user was charged for performed trade.'
},
if: ->(_, options) { options[:current_user] }
) do |trade, options|
fee_amount(trade, trade.order_for_member(options[:current_user]))
end
expose(
:market_id,
as: :market,
documentation: {
type: String,
desc: 'Trade market id.'
}
)
expose(
:created_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'Trade create time in iso8601 format.'
}
)
expose(
:taker_type,
documentation: {
type: String,
desc: 'Trade taker order type (sell or buy).'
}
) do |trade, _options|
trade.taker_order.side
end
expose(
:side,
if: ->(trade, options) { options[:side] || options[:current_user] },
documentation: {
type: String,
desc: 'Trade side.'
}
) do |trade, options|
options[:side] || trade.side(options[:current_user])
end
expose(
:order_id,
documentation: {
type: Integer,
desc: 'Order id.'
},
if: ->(_, options) { options[:current_user] }
) do |trade, options|
trade.order_for_member(options[:current_user]).id
end
def fee_amount(trade, order)
trade.order_fee(order) * (order.side == 'buy' ? trade.amount : trade.total)
end
def fee_currency(order)
order.side == 'buy' ? order.ask : order.bid
end
end
end
end
end

View File

@@ -0,0 +1,65 @@
module API
module V2
module Entities
class TradingFee < API::V2::Entities::Base
expose(
:id,
documentation:{
type: Integer,
desc: 'Unique trading fee table identifier in database.'
}
)
expose(
:group,
documentation:{
type: String,
desc: 'Member group for define maker/taker fee.'
}
)
expose(
:market_id,
documentation:{
type: String,
desc: 'Market id for define maker/taker fee.'
}
)
expose(
:maker,
documentation:{
type: BigDecimal,
desc: 'Market maker fee.'
}
)
expose(
:taker,
documentation:{
type: BigDecimal,
desc: 'Market taker fee.'
}
)
expose(
:created_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'Trading fee table created time in iso8601 format.'
}
)
expose(
:updated_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'Trading fee table updated time in iso8601 format.'
}
)
end
end
end
end

View File

@@ -0,0 +1,105 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Entities
class Transactions < Base
expose(
:address,
documentation: {
type: String,
desc: 'Recipient address of transaction.'
}
)
expose(
:currency_id,
as: :currency,
documentation: {
type: String,
desc: 'Transaction currency id.'
}
)
expose(
:amount,
documentation: {
type: BigDecimal,
desc: 'Transaction amount.'
}
)
expose(
:fee,
documentation: {
type: BigDecimal,
desc: 'Transaction fee.'
}
)
expose(
:txid,
documentation: {
type: String,
desc: 'Transaction id.'
}
)
expose(
:aasm_state,
as: :state,
documentation: {
type: String,
desc: 'Transaction state.'
}
)
expose(
:note,
documentation: {
type: String,
desc: 'Withdraw note.'
}
)
expose(
:confirmations,
expose_nil: false,
documentation: {
type: Integer,
desc: 'Number of confirmations.'
}
)
expose(
:created_at,
format_with: :iso8601,
documentation: {
type: String,
desc: "Transaction created time in iso8601 format."
}
)
expose(
:updated_at,
format_with: :iso8601,
documentation: {
type: String,
desc: "Transaction updated time in iso8601 format."
}
)
expose(
:type,
documentation: {
type: String,
desc: 'Type of transaction'
}
) do |transaction, _options|
transaction[:type].constantize.superclass.to_s
end
end
end
end
end

View File

@@ -0,0 +1,36 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Entities
class Version < Base
expose(:git_sha,
documentation: {
type: String,
desc: 'Running Peatio git commit SHA.'
}
)
expose(:git_tag,
documentation: {
type: String,
desc: 'Running Peatio git tag.'
}
)
expose(:build_date,
format_with: :iso8601,
documentation: {
type: String,
desc: 'Running Peatio build date in iso8601 format'
}
)
expose(:version,
documentation: {
type: String,
desc: 'Running Peatio version'
}
)
end
end
end
end

View File

@@ -0,0 +1,139 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Entities
class Withdraw < Base
expose(
:id,
documentation: {
type: Integer,
desc: 'The withdrawal id.'
}
)
expose(
:txid,
documentation: {
type: String,
desc: 'The withdrawal Transaction id.'
}
)
expose(
:currency_id,
as: :currency,
documentation: {
type: String,
desc: 'The currency code.'
}
)
expose(
:type,
documentation: {
type: String,
desc: 'The withdrawal type'
}
) { |w| w.currency.fiat? ? :fiat : :coin }
expose(
:sum,
as: :amount,
documentation: {
type: String,
desc: 'The withdrawal amount'
}
)
expose(
:fee,
documentation: {
type: BigDecimal,
desc: 'The exchange fee.'
}
)
expose(
:txid,
as: :blockchain_txid,
documentation: {
type: String,
desc: 'The withdrawal transaction id.'
}
)
expose(
:rid,
documentation: {
type: String,
desc: 'The beneficiary ID or wallet address on the Blockchain.'
}
)
expose(
:aasm_state,
as: :state,
documentation: {
type: String,
desc: 'The withdrawal state.'
}
)
expose(
:confirmations,
if: ->(withdraw) { withdraw.currency.coin? },
documentation: {
type: Integer,
desc: 'Number of confirmations.'
}
)
expose(
:note,
documentation: {
type: String,
desc: 'Withdraw note.'
}
)
expose(
:transfer_type,
documentation: {
type: String,
desc: 'Withdraw transfer type'
}
)
expose(
:created_at,
:updated_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'The datetimes for the withdrawal.'
}
)
expose(
:completed_at,
as: :done_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'The datetime when withdraw was completed'
}
)
expose(
:otp,
documentation: {
type: String,
desc: 'The active deactive 2fa'
}
) { |w| w.member.otp.present? ? :true : :false }
end
end
end
end

View File

@@ -0,0 +1,73 @@
module API
module V2
module Entities
class WithdrawLimit < API::V2::Entities::Base
expose(
:id,
documentation:{
type: Integer,
desc: 'Unique withdraw limit table identifier in database.'
}
)
expose(
:group,
documentation:{
type: String,
desc: 'Member group for define withdraw limits.'
}
)
expose(
:kyc_level,
documentation:{
type: String,
desc: 'KYC level for define withdraw limits.'
}
)
expose(
:kind,
documentation:{
type: String,
desc: 'kind for define deposit limits.'
}
)
expose(
:limit_24_hour,
documentation:{
type: BigDecimal,
desc: '24 hours withdraw limit.'
}
)
expose(
:limit_1_month,
documentation:{
type: BigDecimal,
desc: '1 month withdraw limit.'
}
)
expose(
:created_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'Withdraw limit table created time in iso8601 format.'
}
)
expose(
:updated_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'Withdraw limit table updated time in iso8601 format.'
}
)
end
end
end
end

View File

@@ -0,0 +1,39 @@
# encoding: UTF-8
# frozen_string_literal: true
module API::V2
module ExceptionHandlers
def self.included(base)
base.instance_eval do
rescue_from Grape::Exceptions::ValidationErrors do |e|
errors_array = e.full_messages.map do |err|
err.split.last
end
error!({ errors: errors_array }, 422)
end
rescue_from Grape::Exceptions::MethodNotAllowed do |_e|
error!({ errors: 'server.method_not_allowed' }, 405)
end
rescue_from Grape::Exceptions::InvalidMessageBody do |_e|
error!({ errors: 'server.method.invalid_message_body' }, 400)
end
rescue_from Peatio::Auth::Error do |e|
report_exception(e)
error!({ errors: ['jwt.decode_and_verify'] }, 401)
end
rescue_from ActiveRecord::RecordNotFound do |_e|
error!({ errors: ['record.not_found'] }, 404)
end
rescue_from :all do |e|
report_exception(e)
error!({ errors: ['server.internal_error'] }, 500)
end
end
end
end
end

121
app/api/v2/helpers.rb Normal file
View File

@@ -0,0 +1,121 @@
# frozen_string_literal: true
module API
module V2
module Helpers
extend Memoist
def admin_authorize!(action, model, attributes = {})
if attributes.present?
attributes.each do |k, _|
AdminAbility.new(current_user).authorize!(action, model, k)
end
else
AdminAbility.new(current_user).authorize!(action, model)
end
rescue StandardError
error!({ errors: ['admin.ability.not_permitted'] }, 403)
end
def user_authorize!(action, model, attributes = {})
if attributes.present?
attributes.each do |k, _|
UserAbility.new(current_user).authorize!(action, model, k)
end
else
UserAbility.new(current_user).authorize!(action, model)
end
rescue StandardError
error!({ errors: ['user.ability.not_permitted'] }, 403)
end
def authenticate!
current_user || raise(Peatio::Auth::Error)
end
def set_ets_context!
return unless defined?(Raven)
if current_user
Raven.user_context(
email: current_user.email,
uid: current_user.uid,
role: current_user.role
)
end
Raven.tags_context(
peatio_version: Peatio::Application::VERSION
)
end
def deposits_must_be_permitted!
if current_user.level < ENV.fetch('MINIMUM_MEMBER_LEVEL_FOR_DEPOSIT').to_i
error!({ errors: ['account.deposit.not_permitted'] }, 403)
end
end
def withdraws_must_be_permitted!
if current_user.level < ENV.fetch('MINIMUM_MEMBER_LEVEL_FOR_WITHDRAW').to_i
error!({ errors: ['account.withdraw.not_permitted'] }, 403)
end
end
def trading_must_be_permitted!
if current_user.level < ENV.fetch('MINIMUM_MEMBER_LEVEL_FOR_TRADING').to_i
error!({ errors: ['market.trade.not_permitted'] }, 403)
end
end
def withdraw_api_must_be_enabled!
error!({ errors: ['account.withdraw.disabled_api'] }, 422) if ENV.false?('ENABLE_ACCOUNT_WITHDRAWAL_API')
end
def current_user
# jwt.payload provided by rack-jwt
if request.env.key?('jwt.payload')
begin
Member.from_payload(request.env['jwt.payload'].symbolize_keys)
# Handle race conditions when creating member record.
# We do not handle race condition for update operations.
# http://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-find_or_create_by
rescue ActiveRecord::RecordNotUnique
retry
end
end
end
memoize :current_user
def current_market
::Market.active.find_by_id(params[:market])
end
memoize :current_market
def format_ticker(ticker)
permitted_keys = %i[low high open last volume amount
avg_price price_change_percent]
# Add vol for compatibility with old API.
formatted_ticker = ticker.slice(*permitted_keys)
.merge(vol: ticker[:volume])
{ at: ticker[:at],
ticker: formatted_ticker }
end
def paginate(collection, include_total = true)
per_page = params[:limit] || Kaminari.config.default_per_page
per_page = [per_page.to_i, Kaminari.config.max_per_page].compact.min
result = if collection.is_a?(::ActiveRecord::Relation)
collection.page(params[:page].to_i).per(per_page)
elsif collection.is_a?(Array)
Kaminari.paginate_array(collection).page(params[:page].to_i).per(per_page)
end
result.tap do |data|
header 'Total', data.total_count.to_s if include_total
header 'Per-Page', data.limit_value.to_s
header 'Page', data.current_page.to_s
end
end
end
end
end

View File

@@ -0,0 +1,48 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
class Accounts < Grape::API
desc 'Queries the account balance for the given UID and currency.' do
@settings[:scope] = :read_accounts
success API::V2::Management::Entities::Balance
end
params do
requires :uid, type: String, desc: 'The shared user ID.'
requires :currency, type: String, values: -> { Currency.codes(bothcase: true) }, desc: 'The currency code.'
end
post '/accounts/balance' do
member = Member.find_by!(uid: params[:uid])
account = member.get_account(params[:currency])
present account, with: API::V2::Management::Entities::Balance
status 200
end
desc 'Queries the non-zero balance accounts for the given currency.' do
@settings[:scope] = :read_accounts
success API::V2::Management::Entities::Balance
end
params do
requires :currency, type: String, values: -> { Currency.codes(bothcase: true) }, desc: 'The currency code.'
optional :page, type: Integer, default: 1, integer_gt_zero: true, desc: 'The page number (defaults to 1).'
optional :limit, type: Integer, default: 1000, range: 1..100000, desc: 'The number of accounts per page (defaults to 100, maximum is 1000).'
end
post '/accounts/balances' do
accounts = ::Account.where("currency_id = ? AND (balance > 0 OR locked > 0)", params[:currency])
accounts
.order(id: :asc)
.page(params[:page])
.per(params[:limit])
.tap { |q| present q, with: API::V2::Management::Entities::Balance }
status 200
end
end
end
end
end

View File

@@ -0,0 +1,111 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
class Beneficiaries < Grape::API
namespace :beneficiaries do
desc 'Get list of user beneficiaries' do
@settings[:scope] = :read_beneficiaries
success API::V2::Management::Entities::Beneficiary
end
params do
requires :uid,
type: String,
desc: 'The shared user ID.'
optional :currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'management.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: 'management.beneficiary.invalid_state'},
desc: 'Defines either beneficiary active - user can use it to withdraw money'\
'or pending - requires beneficiary activation with pin.'
end
post '/list' do
member = Member.find_by!(uid: params[:uid])
member
.beneficiaries
.available_to_member
.tap { |q| q.where!(currency_id: params[:currency_id]) if params[:currency_id].present? }
.tap {|q| q.where!(state: params[:state]) if params[:state].present? }
.yield_self { |b| present paginate(b), with: API::V2::Management::Entities::Beneficiary }
status 200
end
desc 'Create new beneficiary' do
@settings[:scope] = :write_beneficiaries
success API::V2::Management::Entities::Beneficiary
end
params do
requires :currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'management.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: 'management.beneficiary.too_long_name' },
desc: 'Human rememberable name which refer beneficiary.'
optional :description,
type: String,
values: { value: ->(v) { v.size <= 255 }, message: 'management.beneficiary.too_long_description' },
desc: 'Human rememberable description which refer beneficiary.'
requires :data,
type: { value: JSON, message: 'management.beneficiary.non_json_data' },
allow_blank: false,
desc: 'Beneficiary data in JSON format'
requires :uid,
type: String,
desc: 'The shared user ID.'
optional :state,
type: String,
values: { value: -> { ::Beneficiary::STATES_AVAILABLE_FOR_MEMBER.map(&:to_s) }, message: 'management.beneficiary.invalid_state'},
desc: 'Defines either beneficiary active - user can use it to withdraw money'\
'or pending - requires beneficiary activation with pin.'
end
post do
declared_params = declared(params)
member = Member.find_by!(uid: params[:uid])
currency = Currency.find_by!(id: params[:currency_id])
if !currency.withdrawal_enabled?
error!({ errors: ['management.currency.withdrawal_disabled'] }, 422)
elsif currency.coin? && declared_params.dig(:data, :address).blank?
error!({ errors: ['management.beneficiary.missing_address_in_data'] }, 422)
elsif currency.fiat? && declared_params.dig(:data, :full_name).blank?
error!({ errors: ['management.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? &&
member
.beneficiaries
.available_to_member
.where(currency: currency)
.any? { |b| b.data['address'] == declared_params.dig(:data, :address) }
error!({ errors: ['management.beneficiary.duplicate_address'] }, 422)
end
present member
.beneficiaries
.create!(declared_params.except(:uid)),
with: API::V2::Management::Entities::Beneficiary
rescue ActiveRecord::RecordInvalid => e
report_exception(e)
error!({ errors: ['management.beneficiary.failed_to_create'] }, 422)
end
end
end
end
end
end

View File

@@ -0,0 +1,124 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
class Currencies < Grape::API
# POST: api/v2/management/currencies/list
desc 'Return currencies list.' do
@settings[:scope] = :read_currencies
success API::V2::Management::Entities::Currency
end
params do
optional :type,
type: String,
values: { value: %w[fiat coin], message: 'management.currency.invalid_type' },
desc: -> { API::V2::Entities::Currency.documentation[:type][:desc] }
end
post '/currencies/list' do
currencies = Currency.all
currencies = currencies.where(type: params[:type]).includes(:blockchain) if params[:type] == 'coin'
currencies = currencies.where(type: params[:type]) if params[:type] == 'fiat'
present currencies.ordered, with: API::V2::Entities::Currency
status 200
end
# POST: api/v2/management/currencies
desc 'Returns currency by code.' do
@settings[:scope] = :read_currencies
success API::V2::Management::Entities::Currency
end
params do
requires :code, type: String, desc: 'The currency code.'
end
post '/currencies/:code', requirements: { code: /[\w\.\-]+/ } do
present Currency.find_by!(params.slice(:code)), with: API::V2::Management::Entities::Currency
end
desc 'Update currency.' do
@settings[:scope] = :write_currencies
success API::V2::Management::Entities::Currency
end
params do
requires :id,
type: String,
desc: -> { API::V2::Management::Entities::Currency.documentation[:id][:desc] }
optional :name, desc: -> { API::V2::Management::Entities::Currency.documentation[:name][:desc] }
optional :deposit_fee,
type: { value: BigDecimal, message: 'management.currency.non_decimal_deposit_fee' },
values: { value: -> (p){ p >= 0 }, message: 'management.currency.invalid_deposit_fee' },
default: 0.0,
desc: -> { API::V2::Management::Entities::Currency.documentation[:deposit_fee][:desc] }
optional :min_deposit_amount,
type: { value: BigDecimal, message: 'management.currency.min_deposit_amount' },
values: { value: -> (p){ p >= 0 }, message: 'management.currency.invalid_min_deposit_amount' },
default: 0.0,
desc: -> { API::V2::Management::Entities::Currency.documentation[:min_deposit_amount][:desc] }
optional :min_collection_amount,
type: { value: BigDecimal, message: 'management.currency.non_decimal_min_collection_amount' },
values: { value: -> (p){ p >= 0 }, message: 'management.currency.invalid_min_collection_amount' },
default: 0.0,
desc: -> { API::V2::Management::Entities::Currency.documentation[:min_collection_amount][:desc] }
optional :withdraw_fee,
type: { value: BigDecimal, message: 'management.currency.non_decimal_withdraw_fee' },
values: { value: -> (p){ p >= 0 }, message: 'management.currency.invalid_withdraw_fee' },
default: 0.0,
desc: -> { API::V2::Management::Entities::Currency.documentation[:withdraw_fee][:desc] }
optional :min_withdraw_amount,
type: { value: BigDecimal, message: 'management.currency.non_decimal_min_withdraw_amount' },
values: { value: -> (p){ p >= 0 }, message: 'management.currency.invalid_min_withdraw_amount' },
default: 0.0,
desc: -> { API::V2::Management::Entities::Currency.documentation[:min_withdraw_amount][:desc] }
optional :withdraw_limit_24h,
type: { value: BigDecimal, message: 'management.currency.non_decimal_withdraw_limit_24h' },
values: { value: -> (p){ p >= 0 }, message: 'management.currency.invalid_withdraw_limit_24h' },
default: 0.0,
desc: -> { API::V2::Management::Entities::Currency.documentation[:withdraw_limit_24h][:desc] }
optional :withdraw_limit_72h,
type: { value: BigDecimal, message: 'management.currency.non_decimal_withdraw_limit_72h' },
values: { value: -> (p){ p >= 0 }, message: 'management.currency.invalid_withdraw_limit_72h' },
default: 0.0,
desc: -> { API::V2::Management::Entities::Currency.documentation[:withdraw_limit_72h][:desc] }
optional :position,
type: { value: Integer, message: 'management.currency.non_integer_position' },
values: { value: -> (p){ p >= ::Currency::TOP_POSITION }, message: 'management.currency.invalid_position' },
desc: -> { API::V2::Management::Entities::Currency.documentation[:position][:desc] }
optional :options,
type: { value: JSON, message: 'management.currency.non_json_options' },
default: {},
desc: -> { API::V2::Management::Entities::Currency.documentation[:options][:desc] }
optional :visible,
type: { value: Boolean, message: 'management.currency.non_boolean_visible' },
default: true,
desc: -> { API::V2::Management::Entities::Currency.documentation[:visible][:desc] }
optional :deposit_enabled,
type: { value: Boolean, message: 'management.currency.non_boolean_deposit_enabled' },
default: true,
desc: -> { API::V2::Management::Entities::Currency.documentation[:deposit_enabled][:desc] }
optional :withdrawal_enabled,
type: { value: Boolean, message: 'management.currency.non_boolean_withdrawal_enabled' },
default: true,
desc: -> { API::V2::Management::Entities::Currency.documentation[:withdrawal_enabled][:desc] }
optional :precision,
type: { value: Integer, message: 'management.currency.non_integer_base_precision' },
default: 8,
desc: -> { API::V2::Management::Entities::Currency.documentation[:precision][:desc] }
optional :icon_url, desc: -> { API::V2::Management::Entities::Currency.documentation[:icon_url][:desc] }
end
put '/currencies/update' do
currency = ::Currency.find_by!(params.slice(:id))
if currency.update(declared(params, include_missing: false))
present currency, with: API::V2::Management::Entities::Currency
else
body errors: currency.errors.full_messages
status 422
end
end
end
end
end
end

View File

@@ -0,0 +1,104 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
class Deposits < Grape::API
desc 'Returns deposits as paginated collection.' do
@settings[:scope] = :read_deposits
success API::V2::Management::Entities::Deposit
end
params do
optional :uid, type: String, desc: 'The shared user ID.'
optional :currency, type: String, values: -> { Currency.codes(bothcase: true) }, desc: 'The currency code.'
optional :page, type: Integer, default: 1, integer_gt_zero: true, desc: 'The page number (defaults to 1).'
optional :limit, type: Integer, default: 100, range: 1..1000, desc: 'The number of deposits per page (defaults to 100, maximum is 1000).'
optional :state, type: String, values: -> { ::Deposit.aasm.states.map(&:name).map(&:to_s) }, desc: 'The state to filter by.'
end
post '/deposits' do
currency = Currency.find(params[:currency]) if params[:currency].present?
member = Member.find_by!(uid: params[:uid]) if params[:uid].present?
Deposit
.order(id: :desc)
.tap { |q| q.where!(currency: currency) if currency }
.tap { |q| q.where!(member: member) if member }
.tap { |q| q.where!(aasm_state: params[:state]) if params[:state] }
.includes(:member, :currency)
.page(params[:page])
.per(params[:limit])
.tap { |q| present q, with: API::V2::Management::Entities::Deposit }
status 200
end
desc 'Returns deposit by TID.' do
@settings[:scope] = :read_deposits
success API::V2::Management::Entities::Deposit
end
params do
requires :tid, type: String, desc: 'The transaction ID.'
end
post '/deposits/get' do
present Deposit.find_by!(params.slice(:tid)), with: API::V2::Management::Entities::Deposit
end
desc 'Creates new fiat deposit with state set to «submitted». ' \
'Optionally pass field «state» set to «accepted» if want to load money instantly. ' \
'You can also use PUT /fiat_deposits/:id later to load money or cancel deposit.' do
@settings[:scope] = :write_deposits
success API::V2::Management::Entities::Deposit
end
params do
requires :uid, type: String, desc: 'The shared user ID.'
optional :tid, type: String, desc: 'The shared transaction ID. Must not exceed 64 characters. Peatio will generate one automatically unless supplied.'
requires :currency, type: String, values: -> { Currency.fiats.codes(bothcase: true) }, desc: 'The currency code.'
requires :amount, type: BigDecimal, desc: 'The deposit amount.'
optional :state, type: String, desc: 'The state of deposit.', values: %w[accepted]
optional :transfer_type, type: String,
values: { value: -> { Deposit::TRANSFER_TYPES.keys }, message: 'account.deposit.transfer_type_not_in_list' },
desc: -> { API::V2::Admin::Entities::Deposit.documentation[:transfer_type][:desc] }
end
post '/deposits/new' do
member = Member.find_by(uid: params[:uid])
currency = Currency.find(params[:currency])
unless currency.deposit_enabled?
error!({ errors: ['management.currency.deposit_disabled'] }, 422)
end
data = { member: member, currency: currency }.merge!(params.slice(:amount, :tid, :transfer_type))
deposit = ::Deposits::Fiat.new(data)
if deposit.save
deposit.charge! if params[:state] == 'accepted'
present deposit, with: API::V2::Management::Entities::Deposit
else
body errors: deposit.errors.full_messages
status 422
end
end
desc 'Allows to load money or cancel deposit.' do
@settings[:scope] = :write_deposits
success API::V2::Management::Entities::Deposit
end
params do
requires :tid, type: String, desc: 'The shared transaction ID.'
requires :state, type: String, desc: 'The new state to apply.', values: %w[canceled accepted]
end
put '/deposits/state' do
deposit = ::Deposits::Fiat.find_by!(params.slice(:tid))
if deposit.submitted?
deposit.with_lock do
params[:state] == 'canceled' ? deposit.cancel! : deposit.accept!
end
present deposit, with: API::V2::Management::Entities::Deposit
status 200
else
status 422
end
end
end
end
end
end

View File

@@ -0,0 +1,99 @@
# frozen_string_literal: true
module API
module V2
module Management
class Engines < Grape::API
namespace :engines do
desc 'Get all engine, result is paginated.' do
@settings[:scope] = :read_engines
success API::V2::Management::Entities::Engine
end
params do
optional :limit,
type: { value: Integer, message: 'management.pagination.non_integer_limit' },
values: { value: 1..1000, message: 'management.pagination.invalid_limit' },
default: 100,
desc: 'Limit the number of returned paginations. Defaults to 100.'
optional :page,
type: { value: Integer, message: 'management.pagination.non_integer_page' },
allow_blank: false,
default: 1,
desc: 'Specify the page of paginated results.'
optional :ordering,
values: { value: %w(asc desc), message: 'management.pagination.invalid_ordering' },
default: 'asc',
desc: 'If set, returned values will be sorted in specific order, defaults to \'asc\'.'
optional :order_by,
default: 'id',
desc: 'Name of the field, which result will be ordered by.'
end
post '/get' do
result = ::Engine.order(params[:order_by] => params[:ordering])
present paginate(result), with: API::V2::Management::Entities::Engine
end
desc 'Creates new engine' do
@settings[:scope] = :write_engines
success API::V2::Management::Entities::Engine
end
params do
requires :name,
desc: -> { API::V2::Management::Entities::Engine.documentation[:name][:desc] },
values: { value: ->(v) { !v.in?(::Engine.pluck(:name)) }, message: 'management.engine.duplicate_name' }
requires :driver,
desc: -> { API::V2::Management::Entities::Engine.documentation[:driver][:desc] }
optional :uid,
desc: -> { API::V2::Management::Entities::Engine.documentation[:uid][:desc] }
optional :key,
desc: -> { 'Credentials for remote engine' }
optional :secret,
desc: -> { 'Credentials for remote engine' }
optional :data,
desc: -> { 'Metadata for engine' }
end
post do
engine = ::Engine.new(declared(params))
if engine.save
present engine, with: API::V2::Management::Entities::Engine
status 201
else
body errors: engine.errors.full_messages
status 422
end
end
desc 'Update engine' do
@settings[:scope] = :write_engines
success API::V2::Management::Entities::Engine
end
params do
requires :id,
desc: -> { API::V2::Management::Entities::Engine.documentation[:id][:desc] }
optional :name,
desc: -> { API::V2::Management::Entities::Engine.documentation[:name][:desc] }
optional :driver,
desc: -> { API::V2::Management::Entities::Engine.documentation[:driver][:desc] }
optional :key,
desc: -> { 'Credentials for remote engine' }
optional :secret,
desc: -> { 'Credentials for remote engine' }
optional :state,
values: { value: ::Engine::STATES.values, message: 'management.engine.invalid_state' },
default: 1,
desc: -> { API::V2::Management::Entities::Engine.documentation[:state][:desc] }
end
post '/update' do
engine = ::Engine.find(params[:id])
if engine.update(declared(params, include_missing: false))
present engine, with: API::V2::Management::Entities::Engine
else
body errors: engine.errors.full_messages
status 422
end
end
end
end
end
end
end

View File

@@ -0,0 +1,16 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
module Entities
class Balance < Base
expose(:uid, documentation: { type: String, desc: 'The shared user ID.' }) { |w| w.member.uid }
expose(:balance, documentation: { type: String, desc: 'The account balance.' }, format_with: :decimal)
expose(:locked, documentation: { type: String, desc: 'The locked account balance.' }, format_with: :decimal)
end
end
end
end
end

Some files were not shown because too many files have changed in this diff Show More