Initial commit

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

View File

@@ -0,0 +1,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

View File

@@ -0,0 +1,15 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
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
end

View File

@@ -0,0 +1,20 @@
# frozen_string_literal: true
module API
module V2
module Management
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,80 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
module Entities
class Currency < ::API::V2::Entities::Currency
expose(
:code,
documentation: {
desc: 'Unique currency code.',
type: String
}
)
expose(
:min_collection_amount,
documentation: {
desc: 'Minimal deposit amount that will be collected',
example: -> { ::Currency.visible.first.min_collection_amount }
}
)
expose(
:visible,
documentation: {
type: String,
desc: 'Currency display possibility status (true/false).'
}
)
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(
:position,
documentation: {
type: Integer,
desc: 'Currency position.'
}
)
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,30 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
module Entities
class Deposit < Base
expose :tid, documentation: { type: Integer, desc: 'The shared transaction ID.' }
expose :currency_id, as: :currency, documentation: { type: String, desc: 'The currency code.' }
expose(:uid, documentation: { type: String, desc: 'The shared user ID.' }) { |w| w.member.uid }
expose(:type, documentation: { type: String, desc: 'The deposit type (fiat or coin).' }) { |d| d.class.name.demodulize.underscore }
expose :amount, documentation: { type: String, desc: 'The deposit amount.' }, format_with: :decimal
states = [
'«submitted» initial state.',
'«canceled» deposit has been canceled by outer service.',
'«rejected» deposit has been rejected by outer service..',
'«accepted» deposit has been accepted by outer service, money are loaded.'
]
expose :aasm_state, as: :state, documentation: { type: String, desc: 'The deposit state. ' + states.join(' ') }
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 :txid, as: :blockchain_txid, if: -> (d, _) { d.currency.coin? }, documentation: { type: String, desc: 'The transaction ID on the Blockchain (coin only).' }
expose :confirmations, as: :blockchain_confirmations, if: -> (d, _) { d.currency.coin? }, documentation: { type: String, desc: 'The number of transaction confirmations on the Blockchain (coin only).' }
expose :transfer_type, documentation: { type: String, desc: 'deposit transfer_type.' }
end
end
end
end
end

View File

@@ -0,0 +1,51 @@
# frozen_string_literal: true
module API
module V2
module Management
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,38 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
module Entities
class Market < ::API::V2::Entities::Market
expose(
:position,
documentation: {
type: Integer,
desc: 'Market position.'
}
)
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,66 @@
module API
module V2
module Management
module Entities
class Member < Base
expose(
:uid,
documentation: {
type: String,
desc: 'The shared user ID.'
}
)
expose(
:email,
documentation: {
type: String,
desc: 'User email.'
}
)
expose(
:level,
documentation: {
type: Integer,
desc: 'User level.'
}
)
expose(
:role,
documentation: {
type: String,
desc: 'User role.'
}
)
expose(
:group,
documentation: {
type: String,
desc: 'User group (vip-0, vip-1, etc).'
}
)
expose(
:state,
documentation: {
type: String,
desc: 'User state (active/pending/banned).'
}
)
expose(
:created_at,
format_with: :iso8601,
documentation: {
type: String,
desc: 'User create time in iso8601 format.'
}
)
end
end
end
end
end

View File

@@ -0,0 +1,53 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
module Entities
class Operation < Base
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,
if: ->(operation) { !operation.credit.zero? },
documentation: {
type: String,
desc: 'Operation credit amount.'
}
expose :debit,
if: ->(operation) { !operation.debit.zero? },
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(: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,176 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
module Entities
class Order < Base
expose(
:id,
documentation:{
type: Integer,
desc: "Unique order id."
}
)
expose(
:member_id,
documentation:{
type: Integer,
desc: "Member 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
end

View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
module API
module V2
module Management
module Entities
class PaymentAddress < ::API::V2::Entities::PaymentAddress
expose(
:uid,
documentation: {
type: String,
desc: 'The shared user ID.'
}
) { |w| w.member.uid }
expose(
:remote,
documentation: {
type: String,
desc: 'Payment address remote creation (true/false).'
}
)
end
end
end
end
end

View File

@@ -0,0 +1,130 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
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.'
}
)
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(
:maker_order_id,
documentation: {
type: String,
desc: 'Trade maker order id.'
}
)
expose(
:taker_order_id,
documentation: {
type: String,
desc: 'Trade taker order id.'
}
)
expose(
:maker_member_uid,
documentation: {
type: String,
desc: 'Trade ask member uid.'
}
) do |trade|
trade.maker.uid
end
expose(
:taker_member_uid,
documentation: {
type: String,
desc: 'Trade bid member uid.'
}
) do |trade|
trade.taker.uid
end
expose(
:taker_type,
documentation: {
type: String,
desc: 'Trade maker 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.order_for_member(options[:current_user]).side
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
end
end
end
end
end

View File

@@ -0,0 +1,40 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
module Entities
class Transfer < Base
expose :key,
documentation: {
type: String,
desc: 'Unique Transfer Key.'
}
expose :category,
documentation: {
type: String,
desc: 'Transfer Category.'
}
expose :description,
documentation: {
type: String,
desc: 'Transfer Description'
}
# Expose assets, expenses, liabilities, revenues if present.
::Operations::Account::TYPES.map(&:pluralize).each do |op_t|
expose op_t,
using: Operation,
if: ->(transfer) { transfer.public_send(op_t).present? },
documentation: {
desc: "Transfer #{op_t}"
}
end
end
end
end
end
end

View File

@@ -0,0 +1,36 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
module Entities
class Withdraw < Base
expose :tid, documentation: { type: Integer, desc: 'The shared transaction ID.' }
expose(:uid, documentation: { type: String, desc: 'The shared user ID.' }) { |w| w.member.uid }
expose :currency_id, as: :currency, documentation: { type: String, desc: 'The currency code.' }
expose :note, documentation: { type: String, desc: 'The note for withdraw.' }
expose(:type, documentation: { type: String, desc: 'The withdraw type (fiat or coin).' }) { |w| w.class.name.demodulize.underscore }
expose :amount, documentation: { type: String, desc: 'The withdraw amount excluding fee.' }, format_with: :decimal
expose :fee, documentation: { type: String, desc: 'The exchange fee.' }, format_with: :decimal
expose :rid, documentation: { type: String, desc: 'The beneficiary ID or wallet address on the Blockchain.' }
states = [
'«prepared» initial state, money are not locked.',
'«submitted» withdraw has been allowed by outer service for further validation, money are locked.',
'«canceled» withdraw has been canceled by outer service, money are unlocked.',
'«accepted» system has validated withdraw and queued it for processing by worker, money are locked.',
'«rejected» system has validated withdraw and found errors, money are unlocked.',
'«processing» worker is processing withdraw as the current moment, money are locked.',
'«skipped» worker skipped withdrawal in case of insufficient balance of hot wallet or it absence.',
'«succeed» worker has successfully processed withdraw, money are subtracted from the account.',
'«failed» worker has encountered an unhandled error while processing withdraw, money are unlocked.'
]
expose :aasm_state, as: :state, documentation: { type: String, desc: 'The withdraw state. ' + states.join(' ') }
expose :created_at, format_with: :iso8601, documentation: { type: String, desc: 'The datetime when withdraw was created.' }
expose :txid, as: :blockchain_txid, documentation: { type: String, desc: 'The transaction ID on the Blockchain (coin only).' }, if: -> (w, _) { w.currency.coin? }
expose :transfer_type, documentation: { type: String, desc: 'withdraw transfer_type.' }
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 Exceptions
class Authentication < Base
def status
@options.fetch(:status, 401)
end
end
end
end
end
end

View File

@@ -0,0 +1,34 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
module Exceptions
class Base < StandardError
def initialize(message:, **options)
@options = options
super(message)
end
def debug_message
@options[:debug_message]
end
def headers
@options.fetch(:headers, {})
end
def status
@options.fetch(:status)
end
# Change "#<Exception: message>" to "#<Exception: message (debug_message)>".
def inspect
debug_message.present? ? super.gsub(/>\z/, " (#{debug_message})>") : super
end
end
end
end
end
end

View File

@@ -0,0 +1,125 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
module Helpers
def create_operation!(attrs)
account = ::Operations::Account.find_by(code: attrs.fetch(:code))
if account.scope.member?
create_member_operation!(attrs)
else
create_platform_operation!(attrs)
end
end
def set_ets_context!
Raven.tags_context(
peatio_version: Peatio::Application::VERSION
) if defined?(Raven)
end
private
def create_platform_operation!(attrs)
currency = Currency.find(attrs.fetch(:currency))
klass = ::Operations::Account
.find_by(code: attrs.fetch(:code))
.type
.yield_self { |type| "operations/#{type}" }
.camelize
.constantize
if klass == ::Operations::Revenue && attrs.dig(:uid)
member_id = Member.find_by!(uid: attrs.fetch(:uid)).id
else
member_id = nil
end
if attrs[:credit].present?
klass.credit!({
amount: attrs.fetch(:credit),
currency: currency,
code: attrs.fetch(:code),
member_id: member_id,
reference: attrs[:reference]
}.compact)
elsif attrs[:debit].present?
klass.debit!({
amount: attrs.fetch(:debit),
currency: currency,
code: attrs.fetch(:code),
member_id: member_id,
reference: attrs[:reference]
}.compact)
end
end
def create_member_operation!(attrs)
member = Member.find_by!(uid: attrs.fetch(:uid))
currency = Currency.find(attrs.fetch(:currency))
klass = ::Operations::Account
.find_by(code: attrs.fetch(:code))
.type
.yield_self { |type| "operations/#{type}" }
.camelize
.constantize
if attrs[:credit].present?
amount = attrs.fetch(:credit)
ActiveRecord::Base.transaction do
op = klass.credit!(amount: amount,
currency: currency,
code: attrs.fetch(:code),
member_id: member.id,
reference: attrs[:reference])
credit_legacy_balance!(amount: amount,
member: member,
currency: currency,
account: op.account)
op
end
elsif attrs[:debit].present?
amount = attrs.fetch(:debit)
ActiveRecord::Base.transaction do
op = klass.debit!(amount: amount,
currency: currency,
code: attrs.fetch(:code),
member_id: member.id,
reference: attrs[:reference])
debit_legacy_balance!(amount: amount,
member: member,
currency: currency,
account: op.account)
op
end
end
end
# @deprecated
def credit_legacy_balance!(amount:, member:, currency:, account:)
if account.kind.main?
member.get_account(currency).plus_funds(amount)
elsif account.kind.locked?
member.get_account(currency).plus_funds(amount)
member.get_account(currency).lock_funds(amount)
end
end
# @deprecated
def debit_legacy_balance!(amount:, member:, currency:, account:)
if account.kind.main?
member.get_account(currency).sub_funds(amount)
elsif account.kind.locked?
member.get_account(currency).unlock_and_sub_funds(amount)
end
end
end
end
end
end

View File

@@ -0,0 +1,93 @@
# encoding: UTF-8
# frozen_string_literal: true
require 'stringio'
module API
module V2
module Management
class JWTAuthenticationMiddleware < Grape::Middleware::Base
extend Memoist
def before
return if request.path == '/api/v2/management/swagger'
check_request_method!
check_query_parameters!
check_content_type!
payload = check_jwt!(jwt)
env['rack.input'] = StringIO.new(payload.fetch(:data, {}).to_json)
end
private
def request
Grape::Request.new(env)
end
memoize :request
def jwt
JSON.parse(request.body.read)
rescue => e
raise Exceptions::Authentication, \
message: 'Couldn\'t parse JWT.',
debug_message: e.inspect,
status: 400
end
memoize :jwt
def check_request_method!
unless request.post? || request.put? || request.delete?
raise Exceptions::Authentication, \
message: 'Only POST, PUT, and DELETE verbs are allowed.',
status: 405
end
end
def check_query_parameters!
unless request.GET.empty?
raise Exceptions::Authentication, \
message: 'Query parameters are not allowed.',
status: 400
end
end
def check_content_type!
unless request.content_type == 'application/json'
raise Exceptions::Authentication, \
message: 'Only JSON body is accepted.',
status: 400
end
end
def check_jwt!(jwt)
security_configuration = ::Rails.configuration.x.security_configuration
begin
scope = security_configuration.fetch(:scopes).fetch(security_scope)
keychain = security_configuration
.fetch(:keychain)
.slice(*scope.fetch(:permitted_signers))
.each_with_object({}) { |(k, v), memo| memo[k] = v.fetch(:value) }
result = JWT::Multisig.verify_jwt(jwt, keychain, security_configuration.fetch(:jwt, {}))
rescue => e
raise Exceptions::Authentication, \
message: 'Failed to verify JWT.',
debug_message: e.inspect,
status: 401
end
unless (scope.fetch(:mandatory_signers) - result[:verified]).empty?
raise Exceptions::Authentication, \
message: 'Not enough signatures for the action.',
status: 401
end
result[:payload]
end
def security_scope
request.env['api.endpoint'].options.fetch(:route_options).fetch(:scope)
end
end
end
end
end

View File

@@ -0,0 +1,69 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
class Markets < Grape::API
# PUT: api/v2/management/markets/update
desc 'Update market.' do
@settings[:scope] = :write_markets
success API::V2::Management::Entities::Market
end
params do
requires :id,
type: String,
desc: -> { API::V2::Management::Entities::Market.documentation[:id][:desc] }
optional :state,
type: String,
values: { value: ::Market::STATES },
desc: -> { API::V2::Management::Entities::Market.documentation[:state][:desc] }
optional :min_price,
type: { value: BigDecimal },
values: { value: ->(p) { p >= 0 } },
desc: -> { API::V2::Management::Entities::Market.documentation[:min_price][:desc] }
optional :min_amount,
type: { value: BigDecimal },
values: { value: ->(p) { p >= 0 } },
desc: -> { API::V2::Management::Entities::Market.documentation[:min_amount][:desc] }
optional :amount_precision,
type: { value: Integer },
values: { value: ->(p) { p && p >= 0 } },
desc: -> { API::V2::Management::Entities::Market.documentation[:amount_precision][:desc] }
optional :price_precision,
type: { value: Integer },
values: { value: ->(p) { p && p >= 0 } },
desc: -> { API::V2::Management::Entities::Market.documentation[:price_precision][:desc] }
optional :max_price,
type: { value: BigDecimal },
values: { value: ->(p) { p >= 0 } },
desc: -> { API::V2::Management::Entities::Market.documentation[:max_price][:desc] }
optional :position,
type: { value: Integer },
values: { value: -> (p){ p >= ::Market::TOP_POSITION } },
desc: -> { API::V2::Management::Entities::Market.documentation[:position][:desc] }
end
put '/markets/update' do
market = ::Market.find_by!(params.slice(:id))
if market.update(declared(params, include_missing: false))
present market, with: API::V2::Management::Entities::Market
else
body errors: market.errors.full_messages
status 422
end
end
# POST: api/v2/management/markets/list
desc 'Return markets list.' do
@settings[:scope] = :read_markets
success API::V2::Management::Entities::Market
end
post '/markets/list' do
present ::Market.ordered, with: API::V2::Management::Entities::Market
status 200
end
end
end
end
end

View File

@@ -0,0 +1,65 @@
module API
module V2
module Management
class Members < Grape::API
desc 'Create a member.' do
@settings[:scope] = :write_members
end
params do
requires :email,
type: String,
desc: 'User email.'
requires :uid,
type: String,
desc: 'The shared user ID.'
requires :level,
type: Integer,
desc: 'User level.'
requires :role,
type: String,
desc: 'User role.'
requires :state,
type: String,
desc: 'User state.'
requires :group,
type: String,
desc: 'User group'
end
post '/members' do
declared_params = declared(params)
member = Member.create!(declared_params)
present member, with: Entities::Member
status 200
rescue ActiveRecord::RecordInvalid => e
body errors: e.message
status 422
end
desc 'Set user group.' do
@settings[:scope] = :write_members
end
params do
requires :uid,
type: String,
desc: 'The shared user ID.'
requires :group,
type: String,
desc: 'User gruop'
end
post '/members/group' do
declared_params = declared(params)
member = Member.find_by!(uid: declared_params[:uid])
member.update!(group: declared_params[:group])
present member, with: Entities::Member
status 200
rescue ActiveRecord::RecordInvalid => e
body errors: e.message
status 422
end
end
end
end
end

View File

@@ -0,0 +1,80 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
class Mount < Grape::API
PREFIX = '/management'
format :json
content_type :json, 'application/json'
default_format :json
do_not_route_options!
helpers Management::Helpers
before { set_ets_context! }
rescue_from Management::Exceptions::Base do |e|
Management::Mount.logger.error { e.inspect }
error!(e.message, e.status, e.headers)
end
rescue_from Grape::Exceptions::ValidationErrors do |e|
Management::Mount.logger.error { e.inspect }
Management::Mount.logger.debug { e.full_messages }
error!(e.message, 422)
end
rescue_from ActiveRecord::RecordNotFound do |e|
Management::Mount.logger.error { e.inspect }
error!('Couldn\'t find record.', 404)
end
use Management::JWTAuthenticationMiddleware
mount Management::Accounts
mount Management::Deposits
mount Management::Withdraws
mount Management::Tools
mount Management::Operations
mount Management::Orders
mount Management::Transfers
mount Management::Trades
mount Management::Members
mount Management::TradingFees
mount Management::Currencies
mount Management::Markets
mount Management::Beneficiaries
mount Management::PaymentAddress
mount Management::Engines
# The documentation is accessible at http://localhost:3000/swagger?url=/api/v2/management/swagger
# Add swagger documentation for Peatio Management 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 Management API #{API::V2::Mount::API_VERSION}",
description: 'Management API is server-to-server API with high privileges.',
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::Management::Entities::Balance,
API::V2::Management::Entities::Deposit,
API::V2::Management::Entities::Withdraw,
API::V2::Management::Entities::Operation,
API::V2::Management::Entities::Engine
]
end
end
end
end

View File

@@ -0,0 +1,202 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
class Operations < Grape::API
# POST: api/v2/management/assets
# POST: api/v2/management/expenses
# POST: api/v2/management/revenues
#
# POST: api/v2/management/assets/new
# POST: api/v2/management/expenses/new
# POST: api/v2/management/revenues/new
::Operations::Account::PLATFORM_TYPES.each do |op_type|
op_type_plural = op_type.to_s.pluralize
desc "Returns #{op_type_plural} as paginated collection." do
@settings[:scope] = :read_operations
success API::V2::Management::Entities::Operation
end
params do
optional :currency,
type: String,
values: -> { Currency.codes(bothcase: true) },
desc: 'The currency for operations filtering.'
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 objects per page (defaults to 100, maximum is 1000).'
optional :time_from,
type: Integer,
desc: "An integer represents the seconds elapsed since Unix epoch."\
"If set, only operations after the time will be returned."
optional :time_to,
type: Integer,
desc: "An integer represents the seconds elapsed since Unix epoch."\
"If set, only operations before the time will be returned."
optional :reference_type,
type: String,
desc: "The reference type for operations filtering"
end
post op_type_plural do
currency_id = params.fetch(:currency, nil)
"operations/#{op_type}"
.camelize
.constantize
.order(id: :desc)
.tap { |q| q.where!(currency_id: currency_id) if currency_id }
.tap { |q| q.where!(reference_type: params[:reference_type]) if params[:reference_type].present? }
.tap { |q| q.where!('created_at >= ?', Time.at(params[:time_from])) if params[:time_from].present? }
.tap { |q| q.where!('created_at < ?', Time.at(params[:time_to])) if params[:time_to].present? }
.page(params[:page])
.per(params[:limit])
.tap { |q| present q, with: API::V2::Management::Entities::Operation }
status 200
end
desc "Creates new #{op_type} operation." do
@settings[:scope] = :write_operations
success API::V2::Management::Entities::Operation
end
params do
requires :currency,
type: String,
values: -> { ::Currency.codes(bothcase: true) },
desc: 'The currency code.'
requires :code,
type: Integer,
values: -> { ::Operations::Account.where(type: op_type).pluck(:code) },
desc: 'Operation account code'
optional :debit,
type: BigDecimal,
values: ->(v) { v.to_d.positive? },
desc: 'Operation debit amount.'
optional :credit,
type: BigDecimal,
values: ->(v) { v.to_d.positive? },
desc: 'Operation credit amount.'
exactly_one_of :debit, :credit
end
post "/#{op_type_plural}/new" do
attributes = declared(params)
create_operation!(attributes).tap do |op|
present op, with: Entities::Operation
end
status 200
rescue ActiveRecord::RecordInvalid => e
body errors: e.message
status 422
end
end
# POST: api/v2/management/liabilities
#
# POST: api/v2/management/liabilities/new
::Operations::Account::MEMBER_TYPES.each do |op_type|
op_type_plural = op_type.to_s.pluralize
desc "Returns #{op_type_plural} as paginated collection." do
@settings[:scope] = :read_operations
success API::V2::Management::Entities::Operation
end
params do
optional :currency,
type: String,
values: -> { Currency.codes(bothcase: true) },
desc: 'The currency for operations filtering.'
optional :uid,
type: String,
desc: 'The user ID for operations filtering.'
optional :reference_type,
type: String,
desc: "The reference type for operations filtering"
optional :time_from,
type: Integer,
desc: "An integer represents the seconds elapsed since Unix epoch."\
"If set, only operations after the time will be returned."
optional :time_to,
type: Integer,
desc: "An integer represents the seconds elapsed since Unix epoch."\
"If set, only operations before the time will be returned."
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..10000,
desc: 'The number of objects per page (defaults to 100, maximum is 10000).'
end
post op_type_plural do
currency_id = params.fetch(:currency, nil)
member = Member.find_by!(uid: params[:uid]) if params[:uid].present?
"operations/#{op_type}"
.camelize
.constantize
.order(id: :desc)
.tap { |q| q.where!(currency_id: currency_id) if currency_id }
.tap { |q| q.where!(member: member) if member }
.tap { |q| q.where!(reference_type: params[:reference_type]) if params[:reference_type].present? }
.tap { |q| q.where!('created_at >= ?', Time.at(params[:time_from])) if params[:time_from].present? }
.tap { |q| q.where!('created_at < ?', Time.at(params[:time_to])) if params[:time_to].present? }
.tap { |q| present paginate(q), with: API::V2::Management::Entities::Operation }
status 200
end
desc "Creates new #{op_type} operation." do
@settings[:scope] = :write_operations
success API::V2::Management::Entities::Operation
end
params do
requires :currency,
type: String,
values: -> { ::Currency.codes(bothcase: true) },
desc: 'The currency code.'
requires :code,
type: Integer,
values: -> { ::Operations::Account.where(type: op_type).pluck(:code) },
desc: 'Operation account code'
given code: ->(code) { ::Operations::Account.find_by(code: code).try(:scope).try(:member?) } do
requires :uid,
type: String,
desc: 'The user ID for operation owner.'
end
optional :debit,
type: BigDecimal,
values: ->(v) { v.to_d.positive? },
desc: 'Operation debit amount.'
optional :credit,
type: BigDecimal,
values: ->(v) { v.to_d.positive? },
desc: 'Operation credit amount.'
exactly_one_of :debit, :credit
end
post "/#{op_type_plural}/new" do
attributes = declared(params)
create_operation!(attributes).tap do |op|
present op, with: Entities::Operation
end
status 200
rescue ActiveRecord::RecordInvalid => e
body errors: e.message
status 422
end
end
end
end
end
end

View File

@@ -0,0 +1,112 @@
# frozen_string_literal: true
module API
module V2
module Management
class Orders < Grape::API
helpers ::API::V2::OrderHelpers
desc 'Returns orders' do
@settings[:scope] = :read_orders
success API::V2::Management::Entities::Order
end
params do
optional :uid,
values: { value: ->(v) { Member.exists?(uid: v) }, message: 'management.orders.uid_doesnt_exist' },
desc: 'Filter order by owner uid'
optional :market,
values: { value: -> { ::Market.ids }, message: 'management.orders.market_doesnt_exist' },
desc: -> { API::V2::Management::Entities::Market.documentation[:id][:desc] }
optional :state,
values: { value: -> { ::Order.state.values }, message: 'management.orders.invalid_state' },
desc: 'Filter order by state.'
optional :ord_type,
values: { value: ::Order::TYPES, message: 'management.orders.invalid_ord_type' },
desc: 'Filter order by ord_type.'
end
post '/orders' do
if params[:uid].present?
member = Member.find_by(uid: params[:uid])
params.merge!(member_id: member.id) if member.present?
end
ransack_params = API::V2::Admin::Helpers::RansackBuilder.new(params)
.eq(:ord_type, :state, :member_id)
.translate(market: :market_id)
.build
search = Order.ransack(ransack_params)
present search.result, with: API::V2::Management::Entities::Order
status 200
end
desc 'Cancel specific order' do
@settings[:scope] = :write_orders
success API::V2::Management::Entities::Order
end
params do
requires :id,
type: String,
allow_blank: false,
desc: -> { API::V2::Management::Entities::Order.documentation[:id][:desc] }
end
post '/orders/:id/cancel' do
begin
order = Order.find(params[:id])
order.trigger_cancellation
present order, with: API::V2::Management::Entities::Order
status 200
rescue ActiveRecord::RecordNotFound => e
# RecordNotFound in rescued by ExceptionsHandler.
raise(e)
rescue
error!({ errors: ['management.order.cancel_error'] }, 422)
end
end
desc 'Cancel all open orders' do
@settings[:scope] = :write_orders
success API::V2::Management::Entities::Order
end
params do
optional :uid,
values: { value: ->(v) { Member.exists?(uid: v) }, message: 'management.orders.uid_doesnt_exist' },
desc: 'Filter order by owner uid'
requires :market,
values: { value: -> { ::Market.active.ids }, message: 'management.order.market_doesnt_exist' },
desc: -> { API::V2::Management::Entities::Market.documentation[:id][:desc] }
end
post '/orders/cancel' do
if params[:uid].present?
member = Member.find_by(uid: params[:uid])
params.merge!(member_id: member.id) if member.present?
end
market = ::Market.find(params[:market])
market_engine = market.engine
if market_engine.peatio_engine?
ransack_params = API::V2::Admin::Helpers::RansackBuilder.new(params)
.eq(:member_id, state: 'wait')
.translate(market: :market_id)
.build
orders = Order.ransack(ransack_params).result
orders.map(&:trigger_internal_cancellation)
else
filters = {
market_id: market.id,
member_uid: params[:uid]
}.compact
Order.trigger_bulk_cancel_third_party(market_engine.driver, filters)
end
status 204
end
end
end
end
end

View File

@@ -0,0 +1,70 @@
# frozen_string_literal: true
module API
module V2
module Management
class PaymentAddress < Grape::API
desc 'Create payment address' do
@settings[:scope] = :write_payment_addresses
success API::V2::Management::Entities::PaymentAddress
end
params do
requires :uid,
type: String,
values: { value: ->(v) { Member.exists?(uid: v) }, message: 'management.payment_address.uid_doesnt_exist' },
desc: API::V2::Management::Entities::PaymentAddress.documentation[:uid][:desc]
requires :currency,
type: String,
values: { value: -> { Currency.codes(bothcase: true) }, message: 'management.payment_address.currency_doesnt_exist' },
desc: -> { API::V2::Management::Entities::Currency.documentation[:code][:desc] }
optional :remote,
type: { value: Boolean, message: 'management.payment_address.non_boolean_remote' },
desc: API::V2::Management::Entities::PaymentAddress.documentation[:remote][:desc]
end
post '/deposit_address/new' do
member = Member.find_by(uid: params[:uid]) if params[:uid].present?
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
unless params[:remote].nil?
pa = member.payment_address!(wallet.id, params[:remote])
else
pa = member.payment_address!(wallet.id)
end
wallet_service = WalletService.new(wallet)
begin
pa.with_lock do
next if pa.address.present?
# Supply address ID in case of BitGo address generation if it exists.
result = wallet_service.create_address!(member.uid, pa.details.merge(updated_at: pa.updated_at))
if result.present?
pa.update!(address: result[:address],
secret: result[:secret],
details: result.fetch(:details, {}).merge(pa.details))
end
end
present pa, with: API::V2::Management::Entities::PaymentAddress
status 200
rescue StandardError => e
::Rails.logger.error { "Error: #{e} while generating payment address for #{params[:currency]} for user: #{params[:uid]}" }
error!({ errors: ['management.payment_address.failed_to_generate'] }, 422)
end
end
end
end
end
end

View File

@@ -0,0 +1,18 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
class Tools < Grape::API
desc 'Returns server time in seconds since Unix epoch.' do
@settings[:scope] = :tools
end
post '/timestamp' do
body timestamp: Time.now.iso8601
status 200
end
end
end
end
end

View File

@@ -0,0 +1,38 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
class Trades < Grape::API
desc 'Returns trades as paginated collection.' do
@settings[:scope] = :read_trades
success API::V2::Management::Entities::Trade
end
params do
optional :uid, type: String, desc: 'The shared user ID.'
optional :market, type: String,
values: { value: -> { ::Market.active.ids },
message: 'Market does not exist' }
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 objects per page (defaults to 100, maximum is 1000).'
end
post '/trades' do
market = ::Market.find(params[:market]) if params[:market].present?
member = Member.find_by!(uid: params[:uid]) if params[:uid].present?
Trade
.order(id: :desc)
.includes(:maker, :taker)
.tap { |q| q.where!(market: market) if market }
.tap { |q| q.where!("maker_id = #{member.id} OR taker_id = #{member.id}") if member }
.page(params[:page])
.per(params[:limit])
.tap { |q| present q, with: API::V2::Management::Entities::Trade }
status 200
end
end
end
end
end

View File

@@ -0,0 +1,34 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
class TradingFees < Grape::API
desc 'Returns trading_fees table as paginated collection' do
@settings[:scope] = :read_trading_fees
end
params do
optional :group,
type: String,
desc: 'Member group'
optional :market_id,
type: String,
desc: 'Market id',
values: { value: -> { ::Market.ids.append(::TradingFee::ANY) },
message: 'Market does not exist' }
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 objects per page (defaults to 100, maximum is 1000).'
end
post '/fee_schedule/trading_fees' do
TradingFee
.order(id: :desc)
.tap { |t| t.where!(market_id: params[:market_id]) if params[:market_id] }
.tap { |t| t.where!(group: params[:group]) if params[:group] }
.tap { |q| present paginate(q), with: API::V2::Entities::TradingFee }
status 200
end
end
end
end
end

View File

@@ -0,0 +1,98 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
class Transfers < Grape::API
# TODO: Add endpoints for listing Transfer/Transfers.
desc 'Creates new transfer.' do
@settings[:scope] = :write_transfers
end
params do
requires :key,
type: String,
desc: 'Unique Transfer Key.'
requires :category,
type: String,
desc: 'Transfer Category.'
optional :description,
type: String,
desc: 'Transfer Description.'
requires(:operations, type: Array, allow_blank: false) do
requires :currency,
type: String,
values: -> { Currency.codes(bothcase: true) },
desc: 'Operation currency.'
requires :amount,
type: BigDecimal,
values: ->(v) { v.to_d.positive? },
desc: 'Operation amount.'
requires :account_src, type: Hash do
requires :code,
type: Integer,
values: -> { ::Operations::Account.pluck(:code) },
desc: 'Source Account code.'
given code: ->(code) { ::Operations::Account.find_by(code: code).try(:scope).try(:member?) } do
requires :uid,
type: String,
desc: 'Source Account User ID (for accounts with member scope).'
end
end
requires :account_dst, type: Hash do
requires :code,
type: Integer,
values: -> { ::Operations::Account.pluck(:code) },
desc: 'Destination Account code.'
given code: ->(code) { ::Operations::Account.find_by(code: code).try(:scope).try(:member?) } do
requires :uid,
type: String,
desc: 'Destination Account User ID (for accounts with member scope).'
end
end
end
end
post '/transfers/new' do
declared_params = declared(params)
attrs = declared_params.slice(:key, :category, :description)
declared_params[:operations].each do |op_pair|
currency = Currency.find(op_pair[:currency])
debit_op = op_pair[:account_src].merge(debit: op_pair[:amount], credit: 0.0, currency: currency)
credit_op = op_pair[:account_dst].merge(credit: op_pair[:amount], debit: 0.0, currency: currency)
[debit_op, credit_op].each do |op|
klass = ::Operations.klass_for(code: op['code'])
uid = op.delete(:uid)
op.merge!(member: Member.find_by!(uid: uid)) if uid.present?
type = ::Operations::Account.find_by(code: op[:code]).type
type_plural = type.pluralize
if attrs[type_plural].present?
attrs[type_plural].push(klass.new(op))
else
attrs[type_plural] = [klass.new(op)]
end
end
end
present Transfer.create!(attrs), with: Entities::Transfer
status 201
rescue ActiveRecord::RecordInvalid => e
body errors: e.message
status 422
rescue ::Account::AccountError => e
body errors: "Account balance is insufficient (#{e.message})"
status 422
end
end
end
end
end

View File

@@ -0,0 +1,155 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Management
class Withdraws < Grape::API
helpers do
def perform_action(withdraw, action)
withdraw.with_lock do
case action
when 'process'
withdraw.accept!
# Process fiat withdraw immediately. Crypto withdraws will be processed by workers.
if withdraw.currency.fiat?
withdraw.process!
withdraw.dispatch!
withdraw.success!
end
when 'cancel'
withdraw.cancel!
end
end
end
end
desc 'Returns withdraws as paginated collection.' do
@settings[:scope] = :read_withdraws
success API::V2::Management::Entities::Withdraw
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 objects per page (defaults to 100, maximum is 1000).'
optional :state, type: String, values: -> { Withdraw::STATES.map(&:to_s) }, desc: 'The state to filter by.'
end
post '/withdraws' do
currency = Currency.find(params[:currency]) if params[:currency].present?
member = Member.find_by!(uid: params[:uid]) if params[:uid].present?
Withdraw
.order(id: :desc)
.includes(:member, :currency)
.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] }
.page(params[:page])
.per(params[:limit])
.tap { |q| present q, with: API::V2::Management::Entities::Withdraw }
status 200
end
desc 'Returns withdraw by ID.' do
@settings[:scope] = :read_withdraws
success API::V2::Management::Entities::Withdraw
end
params do
requires :tid, type: String, desc: 'The shared transaction ID.'
end
post '/withdraws/get' do
present Withdraw.find_by!(params.slice(:tid)), with: API::V2::Management::Entities::Withdraw
end
desc 'Creates new withdraw.' do
@settings[:scope] = :write_withdraws
detail 'Creates new withdraw. The behaviours for fiat and crypto withdraws are different. ' \
'Fiat: money are immediately locked, withdraw state is set to «submitted», system workers ' \
'will validate withdraw later against suspected activity, and assign state to «rejected» or «accepted». ' \
'The processing will not begin automatically. The processing may be initiated manually from admin panel or by PUT /management_api/v1/withdraws/action. ' \
'Coin: money are immediately locked, withdraw state is set to «submitted», system workers ' \
'will validate withdraw later against suspected activity, validate withdraw address and ' \
'set state to «rejected» or «accepted». ' \
'Then in case state is «accepted» withdraw workers will perform interactions with blockchain. ' \
'The withdraw receives new state «processing». Then withdraw receives state either «confirming» or «failed».' \
'Then in case state is «confirming» withdraw confirmations workers will perform interactions with blockchain.' \
'Withdraw receives state «succeed» when it receives minimum necessary amount of confirmations.'
success API::V2::Management::Entities::Withdraw
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.'
optional :rid, type: String, desc: 'The beneficiary ID or wallet address on the Blockchain.'
optional :beneficiary_id, type: String, desc: 'ID of Active Beneficiary belonging to user.'
requires :currency, type: String, values: -> { Currency.codes(bothcase: true) }, desc: 'The currency code.'
requires :amount, type: BigDecimal, desc: 'The amount to withdraw.'
optional :note, type: String, desc: 'The note for withdraw.'
optional :action, type: String, values: %w[process], desc: 'The action to perform.'
optional :transfer_type, type: String,
values: { value: -> { Withdraw::TRANSFER_TYPES.keys }, message: 'account.withdraw.transfer_type_not_in_list' },
desc: -> { API::V2::Admin::Entities::Withdraw.documentation[:transfer_type][:desc] }
exactly_one_of :rid, :beneficiary_id
end
post '/withdraws/new' do
member = Member.find_by(uid: params[:uid])
currency = Currency.find(params[:currency])
unless currency.withdrawal_enabled?
error!({ errors: ['management.currency.withdrawal_disabled'] }, 422)
end
beneficiary = Beneficiary.find_by(id: params[:beneficiary_id]) if params[:beneficiary_id].present?
if params[:rid].blank? && beneficiary.blank?
error!({ errors: ['management.beneficiary.doesnt_exist'] }, 422)
elsif params[:rid].blank? && !beneficiary&.active?
error!({ errors: ['management.beneficiary.invalid_state_for_withdrawal'] }, 422)
end
if params[:tid].present?
error!({ errors: ['TID already exist'] }, 422) if Withdraw.where(tid: params[:tid]).present?
end
declared_params = declared(params, include_missing: false).slice(:tid, :rid, :note, :transfer_type).merge(
sum: params[:amount],
member: member,
currency: currency,
tid: params[:tid]
)
declared_params.merge!(beneficiary: beneficiary) if params[:beneficiary_id].present?
withdraw = "withdraws/#{currency.type}".camelize.constantize.new(declared_params)
withdraw.save!
withdraw.with_lock { withdraw.accept! }
perform_action(withdraw, params[:action]) if params[:action]
present withdraw, with: API::V2::Management::Entities::Withdraw
rescue ::Account::AccountError => e
report_api_error(e, request)
error!({ errors: [e.to_s] }, 422)
rescue => e
report_exception(e)
error!({ errors: ['Failed to create withdraw!'] }, 422)
end
desc 'Performs action on withdraw.' do
@settings[:scope] = :write_withdraws
detail '«process» system will lock the money, check for suspected activity, validate recipient address, and initiate the processing of the withdraw. ' \
'«cancel» system will mark withdraw as «canceled», and unlock the money.'
success API::V2::Management::Entities::Withdraw
end
params do
requires :tid, type: String, desc: 'The shared transaction ID.'
requires :action, type: String, values: %w[process cancel], desc: 'The action to perform.'
end
put '/withdraws/action' do
record = Withdraw.find_by!(params.slice(:tid))
perform_action(record, params[:action])
present record, with: API::V2::Management::Entities::Withdraw
end
end
end
end
end