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,77 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Management::Deposits, type: :request do
before do
defaults_for_management_api_v1_security_configuration!
management_api_v1_security_configuration.merge! \
scopes: {
read_accounts: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }
}
end
describe 'get balance' do
def request
post_json '/api/v2/management/accounts/balance', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:data) { { uid: member.uid, currency: 'usd'} }
let(:signers) { %i[alex jeff] }
let(:member) { create(:member, :barong) }
before do
deposit = create(:deposit_usd, member: member)
deposit.accept
end
it 'returns the correct status code' do
request
expect(response).to have_http_status(200)
end
it 'contains the correct response data' do
request
expect(JSON.parse(response.body)).to include(
'balance' => member.get_account(data[:currency]).balance.to_s,
'locked' => member.get_account(data[:currency]).locked.to_s
)
end
end
describe 'get balances' do
def request
post_json '/api/v2/management/accounts/balances', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:data) { { currency: 'usd'} }
let(:signers) { %i[alex jeff] }
let!(:members) { create_list(:member, 12, :barong) }
let!(:deposits) do
members.each do |member|
create(:deposit_usd, member: member).accept
end
end
it 'returns the correct status code' do
request
expect(response).to have_http_status(200)
end
it 'paginates' do
balances = Account.order(id: :asc).pluck(:balance)
data.merge!(page: 1, limit: 4)
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).map { |x| x.fetch('balance').to_f }).to eq balances[0...4].map(&:to_f)
data.merge!(page: 3, limit: 4)
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).map { |x| x.fetch('balance').to_f }).to eq balances[8...12].map(&:to_f)
end
it 'contains the correct response data' do
request
expect(JSON.parse(response.body).map { |x| x.fetch('balance').to_f }).to eq Account.order(id: :asc).pluck(:balance).map(&:to_f)
end
end
end

View File

@@ -0,0 +1,374 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Management::Beneficiaries, type: :request do
before do
defaults_for_management_api_v1_security_configuration!
management_api_v1_security_configuration.merge! \
scopes: {
read_beneficiaries: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
write_beneficiaries: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }
}
end
describe 'beneficiary list' do
def request
post_json '/api/v2/management/beneficiaries/list', multisig_jwt_management_api_v1({ data: beneficiary_data }, *signers)
end
let(:signers) { %i[alex jeff] }
let(:member) { create(:member, :level_3) }
let(:beneficiary_data) do
{
uid: member.uid,
}
end
let!(:pending_beneficiaries_for_member) do
create_list(:beneficiary, 2, member: member, state: :pending)
end
let!(:active_beneficiaries_for_member) do
create_list(:beneficiary, 3, member: member, state: :active)
end
let!(:archived_beneficiaries_for_member) do
create_list(:beneficiary, 2, member: member, state: :archived)
end
let!(:other_member_beneficiaries) do
create_list(:beneficiary, 5)
end
context 'missing required params' do
it do
beneficiary_data.except!(:uid)
request
expect(response.status).to eq 422
expect(JSON.parse(response.body)['error']).to match(/uid is missing/i)
end
end
context 'without currency and state' do
it do
request
expect(response.status).to eq 200
total_for_member = pending_beneficiaries_for_member.count + active_beneficiaries_for_member.count
expect(response_body.size).to eq total_for_member
end
end
context 'non-existing currency' do
it do
beneficiary_data.merge!(currency: :uah)
request
expect(response.status).to eq 422
expect(response.body).to match(/management.currency.doesnt_exist/i)
end
end
context 'existing currency' do
let!(:btc_beneficiaries_for_member) do
create_list(:beneficiary, 3, member: member)
end
it do
beneficiary_data.merge!(currency: :btc)
request
expect(response.status).to eq 200
expect(response_body.all? { |b| b['currency'] == 'btc' }).to be_truthy
end
context 'fiat currency' do
let!(:usd_beneficiary_for_member) { create(:beneficiary, currency: Currency.find('usd'), member: member) }
it do
beneficiary_data.merge!(currency: :usd)
request
expect(response.status).to eq 200
expect(response_body.all? { |b| b['currency'] == 'usd' }).to be_truthy
expect(usd_beneficiary_for_member.id).to eq response_body.first['id']
expect(usd_beneficiary_for_member.data[:account_number]).to eq response_body.first['data']['account_number']
end
end
end
context 'invalid state' do
it do
beneficiary_data.merge!(state: :invalid)
request
expect(response.status).to eq 422
expect(response.body).to match(/management.beneficiary.invalid_state/i)
end
end
context 'existing state' do
it do
beneficiary_data.merge!(state: :pending)
request
expect(response.status).to eq 200
expect(response_body.all? { |b| b['state'] == 'pending' }).to be_truthy
end
end
context 'both currency and state' do
let!(:active_btc_beneficiaries_for_member) do
create_list(:beneficiary, 3, member: member, state: :active)
end
it do
beneficiary_data.merge!(currency: :btc, state: :active)
request
expect(response.status).to eq 200
expect(response_body.all? { |b| b['currency'] == 'btc' && b['state'] == 'active' }).to be_truthy
end
end
end
describe 'create beneficiary' do
def request
post_json '/api/v2/management/beneficiaries', multisig_jwt_management_api_v1({ data: beneficiary_data }, *signers)
end
let(:signers) { %i[alex jeff] }
let(:member) { create(:member, :level_3) }
let(:beneficiary_data) do
{
currency: :btc,
name: 'Personal Bitcoin wallet',
description: 'Multisignature Bitcoin Wallet',
uid: member.uid,
state: 'active',
data: {
address: Faker::Blockchain::Bitcoin.address
}
}
end
context 'invalid params' do
context 'missing required params' do
%i[currency name data uid].each do |rp|
context rp do
it do
beneficiary_data.except!(rp)
request
expect(response.status).to eq 422
expect(JSON.parse(response.body)['error']).to match(/#{rp} is missing/i)
end
end
end
end
context 'currency doesn\'t exist' do
it do
beneficiary_data.merge!(currency: :uah)
request
expect(response.status).to eq 422
expect(response.body).to match(/management.currency.doesnt_exist/i)
end
end
context 'name is too long' do
it do
beneficiary_data.merge!(name: Faker::Lorem.sentence(500))
request
expect(response.status).to eq 422
expect(response.body).to match(/management.beneficiary.too_long_name/i)
end
end
context 'invalid state' do
it do
beneficiary_data.merge!(state: 'test')
request
expect(response.status).to eq 422
expect(response.body).to match(/management.beneficiary.invalid_state/i)
end
end
context 'description is too long' do
it do
beneficiary_data.merge!(description: Faker::Lorem.sentence(500))
request
expect(response.status).to eq 422
expect(response.body).to match(/management.beneficiary.too_long_description/i)
end
end
context 'data has invalid type' do
it do
beneficiary_data.merge!(data: 'data')
request
expect(response.status).to eq 422
expect(response.body).to match(/management.beneficiary.non_json_data/i)
end
end
context 'crypto beneficiary' do
context 'nil address in data' do
it do
beneficiary_data[:data][:address] = nil
request
expect(response.status).to eq 422
expect(response.body).to match(/management.beneficiary.missing_address_in_data/i)
end
end
context 'data without address' do
it do
beneficiary_data[:data].delete(:address)
beneficiary_data[:data][:memo] = :memo
request
expect(response.status).to eq 422
expect(response.body).to match(/management.beneficiary.missing_address_in_data/i)
end
end
context 'disabled withdrawal for currency' do
let(:currency) { Currency.find(:btc) }
before do
currency.update(withdrawal_enabled: false)
end
it do
request
expect(response.status).to eq 422
expect(response.body).to match(/management.currency.withdrawal_disabled/i)
end
end
context 'invalid character in address' do
before do
beneficiary_data[:data][:address] = "'" + Faker::Blockchain::Bitcoin.address
end
it do
request
expect(response.status).to eq 422
expect(response.body).to match(/management.beneficiary.failed_to_create/i)
end
end
context 'duplicated address' do
context 'same currency' do
before do
create(:beneficiary,
member: member,
currency_id: beneficiary_data[:currency],
data: {address: beneficiary_data.dig(:data, :address)})
end
it do
request
expect(response.status).to eq 422
expect(response.body).to match(/management.beneficiary.duplicate_address/i)
end
end
context 'different currencies' do
before do
create(:beneficiary,
member: member,
currency_id: :eth,
data: {address: beneficiary_data.dig(:data, :address)})
end
it do
request
expect(response.status).to eq 201
end
end
context 'truncates spaces in address' do
let(:address) { Faker::Blockchain::Bitcoin.address }
before do
beneficiary_data[:data][:address] = " " + address + " "
end
it do
request
expect(response.status).to eq 201
result = JSON.parse(response.body)
expect(Beneficiary.find(result['id']).data['address']).to eq(address)
end
end
end
context 'destination tag in address' do
before do
beneficiary_data[:data][:address] = Faker::Blockchain::Bitcoin.address + "?dt=4"
end
it do
request
expect(response.status).to eq 201
result = JSON.parse(response.body)
expect(Beneficiary.find(result['id']).data['address']).to eq(beneficiary_data[:data][:address])
end
end
context 'fiat beneficiary' do
let(:beneficiary_data) do
{
currency: :usd,
uid: member.uid,
name: Faker::Bank.name,
description: Faker::Company.catch_phrase,
data: generate(:fiat_beneficiary_data)
}
end
context 'nil address in data' do
it do
beneficiary_data[:data].except!(:address)
request
expect(response.status).to eq 201
expect(response_body['data']).to eq beneficiary_data[:data].with_indifferent_access
end
end
context 'nil data' do
it do
beneficiary_data[:data] = nil
request
expect(response.status).to eq 422
expect(JSON.parse(response.body)['error']).to match(/data is empty/i)
end
end
context 'duplicated address' do
context 'same currency' do
before do
create(:beneficiary,
member: member,
currency_id: beneficiary_data[:currency],
data: beneficiary_data[:data])
end
it do
request
expect(response.status).to eq 201
end
end
end
end
end
end
context 'valid params' do
it 'creates beneficiary for member' do
expect do
request
end.to change{ member.beneficiaries.count }.by(1)
end
it 'creates beneficiary with active state' do
request
expect(response.status).to eq 201
id = response_body['id']
expect(Beneficiary.find_by!(id: id).state).to eq 'active'
expect(Beneficiary.find_by!(id: id).data).to eq response_body['data']
end
end
end
end

View File

@@ -0,0 +1,203 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Management::Currencies, type: :request do
before do
defaults_for_management_api_v1_security_configuration!
management_api_v1_security_configuration.merge! \
scopes: {
read_currencies: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
write_currencies: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }
}
end
describe 'get currency by code' do
def request
post_json "/api/v2/management/currencies/#{currency.code}", multisig_jwt_management_api_v1({ data: {} }, *signers)
end
let(:signers) { %i[alex jeff] }
let(:currency) { Currency.find(:usd) }
it 'returns currency by code' do
request
expect(JSON.parse(response.body).fetch('id')).to eq currency.code
end
context 'currency code with dot' do
let!(:currency) { create(:currency, :xagm_cx) }
it 'returns currency by code' do
request
expect(JSON.parse(response.body).fetch('id')).to eq currency.code
end
end
end
describe 'update currency' do
def request
put_json '/api/v2/management/currencies/update', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:data) { {} }
let(:signers) { %i[alex jeff] }
let(:currency) { Currency.find(:btc) }
it 'should validate deposit_fee param' do
data.merge!(id: currency.id, deposit_fee: -10.0)
request
expect(response).to have_http_status 422
expect(response.body).to match(/management.currency.invalid_deposit_fee/i)
end
it 'should validate min_deposit_amount param' do
data.merge!(id: currency.id, min_deposit_amount: -123.0)
request
expect(response).to have_http_status 422
expect(response.body).to match(/management.currency.invalid_min_deposit_amount/i)
end
it 'should validate min_collection_amount param' do
data.merge!(id: currency.id, min_collection_amount: -100.0)
request
expect(response).to have_http_status 422
expect(response.body).to match(/management.currency.invalid_min_collection_amount/i)
end
it 'should validate withdraw_fee param' do
data.merge!(id: currency.id, withdraw_fee: -100.0)
request
expect(response).to have_http_status 422
expect(response.body).to match(/management.currency.invalid_withdraw_fee/i)
end
it 'should validate min_withdraw_amount param' do
data.merge!(id: currency.id, min_withdraw_amount: -1)
request
expect(response).to have_http_status 422
expect(response.body).to match(/management.currency.invalid_min_withdraw_amount/i)
end
it 'should validate withdraw_limit_24h param' do
data.merge!(id: currency.id, withdraw_limit_24h: -1)
request
expect(response).to have_http_status 422
expect(response.body).to match(/management.currency.invalid_withdraw_limit_24h/i)
end
it 'should validate withdraw_limit_72h param' do
data.merge!(id: currency.id, withdraw_limit_72h: -1)
request
expect(response).to have_http_status 422
expect(response.body).to match(/management.currency.invalid_withdraw_limit_72h/i)
end
it 'should validate options param' do
data.merge!(id: currency.id, options: 'blah-blah')
request
expect(response).to have_http_status 422
expect(response.body).to match(/non_json_options/i)
end
it 'should validate visible param' do
data.merge!(id: currency.id, visible: 'blah-blah')
request
expect(response).to have_http_status 422
expect(response.body).to match(/management.currency.non_boolean_visible/i)
end
it 'should validate position param' do
data.merge!(id: currency.id, position: 0)
request
expect(response).to have_http_status 422
expect(response.body).to match(/management.currency.invalid_position/i)
end
it 'should validate deposit_enabled param' do
data.merge!(id: currency.id, deposit_enabled: 'blah-blah')
request
expect(response).to have_http_status 422
expect(response.body).to match(/management.currency.non_boolean_deposit_enabled/i)
end
it 'should validate withdrawal_enabled param' do
data.merge!(id: currency.id, withdrawal_enabled: 'blah-blah')
request
expect(response).to have_http_status 422
expect(response.body).to match(/management.currency.non_boolean_withdrawal_enabled/i)
end
it 'should check required params' do
request
expect(response).to have_http_status 422
expect(response.body).to match(/id is missing/i)
end
it 'should update currency' do
data.merge!(id: currency.id, visible: 'true', withdraw_fee: '0.1')
request
expect(response).to have_http_status 200
result = JSON.parse(response.body)
expect(result.fetch('id')).to eq currency.id
expect(result.fetch('visible')).to eq true
expect(result.fetch('withdraw_fee')).to eq '0.1'
end
end
describe 'POST /api/v2/management/currencies/list' do
def request
post_json "/api/v2/management/currencies/list", multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:signers) { %i[alex jeff] }
let(:data) { {} }
it 'lists visible currencies' do
request
expect(response).to have_http_status 200
result = JSON.parse(response.body)
expect(result.size).to eq Currency.count
end
it 'lists visible coins' do
data.merge!(type: 'coin')
request
expect(response).to have_http_status 200
result = JSON.parse(response.body)
expect(result.size).to eq Currency.coins.size
end
it 'lists visible fiats' do
data.merge!(type: 'fiat')
request
expect(response).to have_http_status 200
result = JSON.parse(response.body, symbolize_names: true)
expect(result.size).to eq Currency.fiats.size
expect(result.dig(0, :id)).to eq 'usd'
end
it 'returns error in case of invalid type' do
data.merge!(type: 'invalid')
request
expect(response).to have_http_status 422
end
end
end

View File

@@ -0,0 +1,269 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Management::Deposits, type: :request do
before do
defaults_for_management_api_v1_security_configuration!
management_api_v1_security_configuration.merge! \
scopes: {
read_deposits: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
write_deposits: { permitted_signers: %i[alex jeff james], mandatory_signers: %i[alex jeff] }
}
end
describe 'list deposits' do
def request
post_json '/api/v2/management/deposits', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:data) { {} }
let(:signers) { %i[alex jeff] }
let(:members) { create_list(:member, 2, :barong) }
before do
Deposit::STATES.tap do |states|
(states.count * 2).times do
create(:deposit_btc, member: members.sample, aasm_state: states.sample)
create(:deposit_usd, member: members.sample, aasm_state: states.sample)
end
end
end
it 'returns deposits' do
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).map { |x| x.fetch('tid') }).to eq Deposit.order(id: :desc).pluck(:tid)
end
it 'paginates' do
ids = Deposit.order(id: :desc).pluck(:tid)
data.merge!(page: 1, limit: 4)
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).map { |x| x.fetch('tid') }).to eq ids[0...4]
data.merge!(page: 3, limit: 4)
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).map { |x| x.fetch('tid') }).to eq ids[8...12]
end
it 'filters by state' do
Deposit::STATES.each do |state|
data.merge!(state: state)
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).count).to eq Deposit.where(aasm_state: state).count
end
end
it 'filters by member' do
member = members.last
data.merge!(uid: member.uid)
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).count).to eq member.deposits.count
end
it 'filters by currency' do
data.merge!(currency: :usd)
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).count).to eq Deposit.where(currency_id: :usd).count
end
end
describe 'create deposits' do
let(:member) { create(:member, :barong) }
let(:currency) { Currency.find(:usd) }
let!(:account) { member.get_account(:usd) }
let(:amount) { 750.77 }
let :data do
{ uid: member.uid,
currency: currency.code,
amount: amount.to_s }
end
let(:signers) { %i[alex jeff] }
def request
post_json '/api/v2/management/deposits/new', multisig_jwt_management_api_v1({ data: data }, *signers)
end
it 'creates new fiat deposit with state «submitted»' do
request
expect(response.status).to eq 201
record = Deposit.find_by_tid!(JSON.parse(response.body).fetch('tid'))
expect(record.amount).to eq 750.77
expect(record.aasm_state).to eq 'submitted'
expect(record.account).to eq member.get_account(currency)
expect(response_body['transfer_type']).to eq 'fiat'
end
it 'can create fiat deposit and immediately accept it' do
data.merge!(state: :accepted)
expect { request }.to change { member.get_account(currency).balance }.by amount
end
it 'denies access unless enough signatures are supplied' do
data.merge!(state: :accepted)
signers.clear.concat %i[james jeff]
expect { request }.not_to(change { member.get_account(currency).balance })
expect(response.status).to eq 401
end
it 'validates member' do
data.delete(:uid)
request
expect(response.body).to match(/uid is missing/i)
data[:uid] = '1234567890'
request
expect(response.body).to match(/member must exist/i)
end
it 'validates currency' do
data.delete(:currency)
request
expect(response.body).to match(/currency is missing/i)
data[:currency] = 'btc'
request
expect(response.body).to match(/currency does not have a valid value/i)
end
it 'validates amount' do
data.delete(:amount)
request
expect(response.body).to match(/amount is missing/i)
data[:amount] = '-340.50'
request
expect(response.body).to match(/amount must be greater than 0/i)
end
it 'validates state' do
data[:state] = 'submitted'
request
expect(response.body).to match(/state does not have a valid value/i)
end
context 'when coin instead of fiat is supplied' do
let(:currency) { Currency.find(:btc) }
it 'doesn\'t work' do
request
expect(response.body).to match(/currency does not have a valid value/i)
end
end
context 'extremely precise values' do
it 'keeps precision for amount' do
data.merge!(amount: '0.0000000123456789')
request
expect(response).to have_http_status(201)
expect(Deposit.last.amount.to_s).to eq data[:amount]
end
end
context 'disabled currency' do
before do
currency.update(deposit_enabled: false)
end
it 'returns error for enabled disabled deposit' do
request
expect(response).to have_http_status(422)
expect(response).to include_api_error('management.currency.deposit_disabled')
end
end
end
describe 'get deposit' do
def request
post_json '/api/v2/management/deposits/get', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:signers) { %i[alex jeff] }
let(:data) { { tid: record.tid } }
let(:record) { create(:deposit_btc, member: member) }
let(:member) { create(:member, :barong) }
it 'returns deposit by TID' do
request
expect(JSON.parse(response.body).fetch('tid')).to eq record.tid
end
end
describe 'update deposit' do
def request
put_json '/api/v2/management/deposits/state', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:currency) { Currency.find(:usd) }
let(:member) { create(:member, :barong) }
let(:amount) { '500.90' }
let(:signers) { %i[alex jeff] }
let(:data) { { tid: record.tid } }
let(:account) { member.get_account(currency) }
let(:record) { Deposits::Fiat.create!(member: member, amount: amount, currency: currency) }
context 'coin deposit' do
let(:record) { create(:deposit_btc) }
it 'works only with fiat deposits' do
data.merge!(state: :accepted)
request
expect(response).to have_http_status(404)
end
end
it 'cancels deposit' do
data.merge!(state: :canceled)
request
expect(response).to have_http_status(200)
expect(Deposit.find_by_tid!(JSON.parse(response.body).fetch('tid')).aasm_state).to eq 'canceled'
account.reload
expect(account.balance).to eq 0
expect(account.locked).to eq 0
end
it 'accepts deposit' do
data.merge!(state: :accepted)
request
expect(response).to have_http_status(200)
expect(Deposit.find_by_tid!(JSON.parse(response.body).fetch('tid')).aasm_state).to eq 'accepted'
account.reload
expect(account.balance).to eq amount.to_d
expect(account.locked).to eq 0
end
it 'doesn\'t cancel deposit twice' do
record.cancel!
expect(record.aasm_state).to eq 'canceled'
data.merge!(state: :canceled)
expect { request }.not_to(change { record.reload.aasm_state })
expect(response).to have_http_status(422)
expect(record.account.balance).to eq 0
expect(record.account.locked).to eq 0
end
it 'doesn\'t accept deposit twice' do
record.accept!
expect(record.aasm_state).to eq 'accepted'
data.merge!(state: :accepted)
expect { request }.not_to(change { record.reload.aasm_state })
expect(response).to have_http_status(422)
expect(record.account.balance).to eq amount.to_d
expect(record.account.locked).to eq 0
end
it 'validates state' do
data.merge!(state: :rejected)
request
expect(response).to have_http_status(422)
expect(response.body).to match(/state does not have a valid value/i)
end
it 'denies access unless enough signatures are supplied' do
data.merge!(state: :accepted)
signers.clear.concat %i[james]
expect { request }.not_to(change { member.get_account(:usd).balance })
expect(response.status).to eq 401
end
end
end

View File

@@ -0,0 +1,191 @@
# frozen_string_literal: true
describe API::V2::Management::Engines, type: :request do
before do
defaults_for_management_api_v1_security_configuration!
management_api_v1_security_configuration.merge! \
scopes: {
read_engines: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
write_engines: { permitted_signers: %i[alex jeff james], mandatory_signers: %i[alex jeff] }
}
end
describe 'POST /engines/get' do
def request
post_json "/api/v2/management/engines/get", multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:signers) { %i[alex jeff] }
let(:data) { params.merge(scope: :read_engines) }
let(:params) do
engines_params
end
context do
let(:engines_params) { {} }
it 'lists of engines' do
request
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.size).to eq 2
end
end
context do
let(:engines_params) do
{
ordering: 'asc'
}
end
it 'returns engines by ascending order' do
request
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result.first['id']).to eq Engine.first.id
end
end
context do
let(:engines_params) do
{
limit: 1,
page: 1
}
end
it 'returns paginated engines' do
request
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '2'
expect(result.size).to eq 1
params[:page] = 2
request
result = JSON.parse(response.body)
expect(response).to be_successful
expect(response.headers.fetch('Total')).to eq '2'
expect(result.size).to eq 1
end
end
end
describe 'POST /engines' do
def request
post_json "/api/v2/management/engines", multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:signers) { %i[alex jeff] }
let(:data) { params.merge(scope: :write_engines) }
let(:params) do
engines_params
end
context do
let(:engines_params) do
{
name: 'new-engine',
driver: 'new_driver',
uid: 'UID123456',
key: 'your_key',
secret: 'your_secret',
data: { some_data: 'some data' }
}
end
it 'creates new engine' do
request
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['name']).to eq 'new-engine'
expect(result['data'].blank?).to eq true
request
expect(response).to have_http_status 422
result = JSON.parse(response.body)
expect(result['error']).to include('management.engine.duplicate_name')
end
end
context do
let(:engines_params) { {} }
it 'checked required params' do
request
expect(response).to have_http_status 422
result = JSON.parse(response.body)
expect(result['error']).to include('name is missing, driver is missing')
end
end
end
describe 'POST /engines/update' do
def request
post_json "/api/v2/management/engines/update", multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:signers) { %i[alex jeff] }
let(:data) { params.merge(scope: :write_engines) }
let(:params) do
engines_params
end
let!(:engine) { create(:engine) }
context do
let(:engines_params) do
{
id: engine.id,
name: 'Second Engine',
driver: 'second_driver'
}
end
it 'updates attributes' do
request
result = JSON.parse(response.body)
expect(response).to be_successful
expect(result['name']).to eq 'Second Engine'
expect(result['driver']).to eq 'second_driver'
end
end
context do
let(:engines_params) do
{
id: engine.id,
name: 'Second Engine',
secret: 'my_secret'
}
end
it 'updates secret' do
request
expect(response).to be_successful
engine.reload
expect(engine.secret).to eq('my_secret')
end
end
context do
let(:engines_params) { {} }
it 'checkes required params' do
request
expect(response).to have_http_status 422
result = JSON.parse(response.body)
expect(result['error']).to include('id is missing')
end
end
end
end

View File

@@ -0,0 +1,15 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Management::Entities::Balance do
let(:member) { create(:member, :barong) }
let(:record) { member.get_account(:usd) }
before { record.update!(balance: 1000.85, locked: 330.55) }
subject { OpenStruct.new API::V2::Management::Entities::Balance.represent(record).serializable_hash }
it do
expect(subject.uid).to eq record.member.uid
expect(subject.balance).to eq '1000.85'
expect(subject.locked).to eq '330.55'
end
end

View File

@@ -0,0 +1,42 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Management::Entities::Deposit do
context 'fiat' do
let(:record) { create(:deposit_usd, member: create(:member, :barong)) }
subject { OpenStruct.new API::V2::Management::Entities::Deposit.represent(record).serializable_hash }
it do
expect(subject.tid).to eq record.tid
expect(subject.currency).to eq 'usd'
expect(subject.uid).to eq record.member.uid
expect(subject.type).to eq 'fiat'
expect(subject.amount).to eq record.amount.to_s
expect(subject.state).to eq record.aasm_state
expect(subject.created_at).to eq record.created_at.iso8601
expect(subject.completed_at).to eq record.completed_at&.iso8601
expect(subject.respond_to?(:blockchain_txid)).to be_falsey
expect(subject.respond_to?(:confirmations)).to be_falsey
end
end
context 'coin' do
let(:record) { create(:deposit_btc, member: create(:member, :barong)) }
subject { OpenStruct.new API::V2::Management::Entities::Deposit.represent(record).serializable_hash }
it do
expect(subject.tid).to eq record.tid
expect(subject.currency).to eq 'btc'
expect(subject.uid).to eq record.member.uid
expect(subject.type).to eq 'coin'
expect(subject.amount).to eq record.amount.to_s
expect(subject.state).to eq record.aasm_state
expect(subject.created_at).to eq record.created_at.iso8601
expect(subject.completed_at).to eq record.completed_at&.iso8601
expect(subject.blockchain_txid).to eq record.txid
expect(subject.blockchain_confirmations).to eq record.confirmations
end
end
end

View File

@@ -0,0 +1,64 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Management::Entities::Operation do
Operations::Account::PLATFORM_TYPES.each do |op_type|
context op_type do
let(:record) { create(op_type) }
subject { OpenStruct.new API::V2::Management::Entities::Operation.represent(record).serializable_hash }
it do
expect(subject.code).to eq record.code
expect(subject.currency).to eq record.currency_id
expect(subject.created_at).to eq record.created_at.iso8601
end
context 'credit' do
it do
expect(subject.credit).to eq record.credit
expect(subject.respond_to?(:debit)).to be_falsey
end
end
context 'debit' do
let(:record) { create(:asset, :debit) }
it do
expect(subject.debit).to eq record.debit
expect(subject.respond_to?(:credit)).to be_falsey
end
end
end
end
Operations::Account::MEMBER_TYPES.each do |op_type|
context op_type do
let(:record) { create(op_type, :with_member) }
subject { OpenStruct.new API::V2::Management::Entities::Operation.represent(record).serializable_hash }
it do
expect(subject.code).to eq record.code
expect(subject.currency).to eq record.currency_id
expect(subject.uid).to eq record.member.uid
expect(subject.created_at).to eq record.created_at.iso8601
end
context 'credit' do
it do
expect(subject.credit).to eq record.credit
expect(subject.respond_to?(:debit)).to be_falsey
end
end
context 'debit' do
let(:record) { create(:asset, :debit) }
it do
expect(subject.debit).to eq record.debit
expect(subject.respond_to?(:credit)).to be_falsey
end
end
end
end
end

View File

@@ -0,0 +1,43 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Management::Entities::Trade do
let(:trade) do
create :trade, :btcusd, maker_order: create(:order_ask, :btcusd), taker_order: create(:order_bid, :btcusd)
end
subject { OpenStruct.new API::V2::Management::Entities::Trade.represent(trade, side: 'sell').serializable_hash }
it do
expect(subject.id).to eq trade.id
expect(subject.order_id).to be_nil
expect(subject.price).to eq trade.price
expect(subject.amount).to eq trade.amount
expect(subject.total).to eq trade.total
expect(subject.market).to eq trade.market_id
expect(subject.side).to eq 'sell'
expect(subject.created_at).to eq trade.created_at.iso8601
expect(subject.maker_order_id).to eq trade.maker_order_id
expect(subject.maker_order_id).to eq trade.maker_order_id
expect(subject.maker_member_uid).to eq trade.maker.uid
expect(subject.taker_member_uid).to eq trade.taker.uid
end
context 'sell order maker' do
it { expect(subject.taker_type).to eq 'buy' }
end
context 'buy order maker' do
let(:trade) do
create :trade, :btcusd, maker_order: create(:order_bid, :btcusd), taker_order: create(:order_ask, :btcusd)
end
it { expect(subject.taker_type).to eq 'sell' }
end
context 'empty side' do
subject { OpenStruct.new API::V2::Management::Entities::Trade.represent(trade).serializable_hash }
it { expect(subject.respond_to?(:side)).to be_falsey }
end
end

View File

@@ -0,0 +1,69 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Management::Entities::Transfer do
let(:record) { create(:transfer) }
subject { OpenStruct.new API::V2::Management::Entities::Transfer.represent(record.reload).serializable_hash }
it do
expect(subject.key).to eq record.key
expect(subject.category).to eq record.category
expect(subject.description).to eq record.description
end
context 'with operations' do
let(:record) { create(:transfer_with_operations) }
it do
::Operations::Account::TYPES.map(&:pluralize).each do |op_t|
expect(subject.respond_to?(op_t)).to be_truthy
end
end
it do
record_assets = API::V2::Management::Entities::Operation
.represent(record.reload.assets)
expect(subject.assets.to_json).to eq record_assets.to_json
end
it do
record_expenses = API::V2::Management::Entities::Operation
.represent(record.reload.expenses)
expect(subject.expenses.to_json).to eq record_expenses.to_json
end
it do
record_liabilities = API::V2::Management::Entities::Operation
.represent(record.reload.liabilities)
expect(subject.liabilities.to_json).to eq record_liabilities.to_json
end
it do
record_revenues = API::V2::Management::Entities::Operation
.represent(record.reload.revenues)
expect(subject.revenues.to_json).to eq record_revenues.to_json
end
end
context 'with single operation type' do
let(:record) { create(:transfer, :with_liabilities) }
it do
expect(subject.respond_to?(:liabilities)).to be_truthy
end
it do
# TYPES - 'liabilities' = PLATFORM_TYPES
::Operations::Account::PLATFORM_TYPES.map(&:pluralize).each do |op_t|
expect(subject.respond_to?(op_t.to_sym)).to be_falsey
end
end
end
context 'without operations' do
it do
::Operations::Account::TYPES.map(&:pluralize).each do |op_t|
expect(subject.respond_to?(op_t)).to be_falsey
end
end
end
end

View File

@@ -0,0 +1,48 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Management::Entities::Withdraw do
context 'fiat' do
let(:rid) { Faker::Bank.iban }
let(:member) { create(:member, :barong) }
let(:record) { create(:usd_withdraw, :with_deposit_liability, member: member, rid: rid) }
subject { OpenStruct.new API::V2::Management::Entities::Withdraw.represent(record).serializable_hash }
it do
expect(subject.tid).to eq record.tid
expect(subject.rid).to eq rid
expect(subject.currency).to eq 'usd'
expect(subject.uid).to eq record.member.uid
expect(subject.type).to eq 'fiat'
expect(subject.amount).to eq record.amount.to_s
expect(subject.note).to eq record.note
expect(subject.fee).to eq record.fee.to_s
expect(subject.respond_to?(:txid)).to be_falsey
expect(subject.state).to eq record.aasm_state
expect(subject.created_at).to eq record.created_at.iso8601
end
end
context 'coin' do
let(:rid) { Faker::Blockchain::Bitcoin.address }
let(:member) { create(:member, :barong) }
let(:record) { create(:btc_withdraw, :with_deposit_liability, member: member, rid: rid) }
subject { OpenStruct.new API::V2::Management::Entities::Withdraw.represent(record).serializable_hash }
it do
expect(subject.tid).to eq record.tid
expect(subject.rid).to eq rid
expect(subject.currency).to eq 'btc'
expect(subject.uid).to eq record.member.uid
expect(subject.type).to eq 'coin'
expect(subject.amount).to eq record.amount.to_s
expect(subject.note).to eq record.note
expect(subject.fee).to eq record.fee.to_s
expect(subject.blockchain_txid).to eq record.txid
expect(subject.state).to eq record.aasm_state
expect(subject.created_at).to eq record.created_at.iso8601
end
end
end

View File

@@ -0,0 +1,8 @@
describe API::V2::Management::Exceptions::Base do
it do
expect(API::V2::Management::Exceptions::Base.new(message: 'Wrong argument.').inspect).to eq \
'#<API::V2::Management::Exceptions::Base: Wrong argument.>'
expect(API::V2::Management::Exceptions::Base.new(message: 'Wrong argument.', debug_message: 'Debug message.').inspect).to eq \
'#<API::V2::Management::Exceptions::Base: Wrong argument. (Debug message.)>'
end
end

View File

@@ -0,0 +1,120 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Management::JWTAuthenticationMiddleware, type: :request do
let(:member) { create(:member, :level_3) }
let(:config) { management_api_v1_security_configuration }
before do
defaults_for_management_api_v1_security_configuration!
config.merge! \
scopes: {
tools: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }
}
end
it 'works in standard conditions' do
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({}, :alex)
expect(response).to be_successful
end
it 'allows only POST, PUT, and DELETE' do
get '/api/v2/management/timestamp'
expect(response).to have_http_status(405)
patch '/api/v2/management/timestamp'
expect(response).to have_http_status(405)
head '/api/v2/management/timestamp'
expect(response).to have_http_status(405)
end
it 'doesn\'t allow query parameters' do
post '/api/v2/management/timestamp?foo=baz&baz=qux'
expect(response).to have_http_status(400)
expect(response.body).to match(/query parameters/i)
end
it 'requires JSON in the request body' do
post '/api/v2/management/timestamp', params: { foo: 'baz', baz: 'qux' }
expect(response).to have_http_status(400)
expect(response.body).to match(/only json/i)
end
it 'denies access when not enough signatures are supplied' do
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({})
expect(response).to have_http_status(401)
expect(response.body).to match(/not enough signatures/i)
end
it 'denies access when token is expired' do
config[:jwt][:verify_expiration] = true
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({ exp: 1.minute.ago.to_i }, :alex)
expect(response).to have_http_status(401)
expect(response.body).to match(/failed to verify jwt/i)
end
context 'valid issuer' do
before { config[:jwt][:verify_iss] = true }
before { config[:jwt].merge!(iss: 'qux') }
it 'validates issuer' do
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({ iss: 'qux' }, :alex)
expect(response).to be_successful
end
end
context 'invalid issuer' do
before { config[:jwt][:verify_iss] = true }
before { config[:jwt].merge!(iss: 'qux') }
it 'validates issuer' do
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({ iss: 'hacker' }, :alex)
expect(response).to have_http_status(401)
expect(response.body).to match(/failed to verify jwt/i)
end
end
context 'valid audience' do
before { config[:jwt][:verify_aud] = true }
before { config[:jwt].merge!(aud: 'qux') }
it 'validates audience' do
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({ aud: 'qux' }, :alex)
expect(response).to be_successful
end
end
context 'invalid audience' do
before { config[:jwt][:verify_aud] = true }
before { config[:jwt].merge!(aud: 'qux') }
it 'validates audience' do
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({ aud: 'hacker' }, :alex)
expect(response).to have_http_status(401)
expect(response.body).to match(/failed to verify jwt/i)
end
end
context 'missing JWT ID' do
before { config[:jwt][:verify_jti] = true }
it 'requires JTI' do
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({}, :alex)
expect(response).to have_http_status(401)
expect(response.body).to match(/failed to verify jwt/i)
end
end
context 'issued at in future' do
before { config[:jwt][:verify_iat] = true }
it 'denies access' do
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({ iat: 200.seconds.from_now.to_i }, :alex)
expect(response).to have_http_status(401)
expect(response.body).to match(/failed to verify jwt/i)
end
end
context 'issued at before future' do
before { config[:jwt][:verify_iat] = true }
it 'allows access' do
post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v1({ iat: 3.seconds.ago.to_i }, :alex)
expect(response).to have_http_status(200)
end
end
end

View File

@@ -0,0 +1,124 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Management::Markets, type: :request do
before do
defaults_for_management_api_v1_security_configuration!
management_api_v1_security_configuration.merge! \
scopes: {
write_markets: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
read_markets: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
}
end
describe 'update market' do
def request
put_json '/api/v2/management/markets/update', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:data) { {} }
let(:signers) { %i[alex jeff] }
let(:market) { Market.find(:btcusd) }
it 'should validate min_price param' do
data.merge!(id: market.id, min_price: -10.0)
request
expect(response).to have_http_status 422
expect(response.body).to match(/min_price does not have a valid value/i)
end
it 'should validate min_amount param' do
data.merge!(id: market.id, min_amount: -123.0)
request
expect(response).to have_http_status 422
expect(response.body).to match(/min_amount does not have a valid value/i)
end
it 'should validate amount_precision param' do
data.merge!(id: market.id, amount_precision: -100.0)
request
expect(response).to have_http_status 422
expect(response.body).to match(/amount_precision does not have a valid value/i)
end
it 'should validate price_precision param' do
data.merge!(id: market.id, price_precision: -100.0)
request
expect(response).to have_http_status 422
expect(response.body).to match(/price_precision does not have a valid value/i)
end
it 'should validate max_price param' do
data.merge!(id: market.id, max_price: -1)
request
expect(response).to have_http_status 422
expect(response.body).to match(/max_price does not have a valid value/i)
end
it 'should validate position param' do
data.merge!(id: market.id, position: -100.0)
request
expect(response).to have_http_status 422
expect(response.body).to match(/position does not have a valid value/i)
end
it 'should validate state param' do
data.merge!(id: market.id, state: 'blah-blah')
request
expect(response).to have_http_status 422
expect(response.body).to match(/state does not have a valid value/i)
end
it 'should check required params' do
request
expect(response).to have_http_status 422
expect(response.body).to match(/id is missing/i)
end
it 'should update market' do
data.merge!(id: market.id, state: 'disabled', min_amount: '0.1')
request
expect(response).to have_http_status 200
result = JSON.parse(response.body)
expect(result.fetch('id')).to eq market.id
expect(result.fetch('state')).to eq 'disabled'
expect(result.fetch('min_amount')).to eq '0.1'
end
end
describe 'fetch markets list' do
def request
post_json '/api/v2/management/markets/list', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:data) { {} }
let(:signers) { %i[alex jeff] }
let(:expected_keys) do
%w[id name base_unit quote_unit min_price max_price
min_amount amount_precision price_precision state position created_at updated_at]
end
it 'lists enabled markets' do
request
expect(response).to have_http_status 200
result = JSON.parse(response.body)
expect(result.size).to eq Market.count
result.each do |market|
expect(market.keys).to eq expected_keys
end
end
end
end

View File

@@ -0,0 +1,93 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Management::Members, type: :request do
before do
defaults_for_management_api_v1_security_configuration!
management_api_v1_security_configuration.merge! \
scopes: {
write_members: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex jeff] },
}
end
describe 'create member' do
def request
post_json '/api/v2/management/members', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:data) { build(:member).slice(:uid, :email, :level, :role, :group, :state) }
let(:signers) { %i[alex jeff] }
it 'returns member' do
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body)).to include( data )
end
context 'invalid params' do
context 'email' do
it 'returns status 422 and error' do
data[:email] = 'fake_email'
request
expect(response).to have_http_status(422)
expect(JSON.parse(response.body)['errors']).to eq("Validation failed: Email is invalid")
end
end
context 'level' do
it 'returns status 422 and error' do
data[:level] = 'fake_level'
request
expect(response).to have_http_status(422)
expect(JSON.parse(response.body)['error']).to eq("level is invalid")
end
end
context 'role' do
it 'returns status 422 and error' do
data[:role] = 'fake_role'
request
expect(response).to have_http_status(422)
expect(JSON.parse(response.body)['errors']).to eq("Validation failed: Role is not included in the list")
end
end
end
end
describe 'set user group' do
def request
post_json '/api/v2/management/members/group', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:data) { {uid: member.uid, group: 'vip-1'} }
let(:signers) { %i[alex jeff] }
let(:member) { create(:member, :barong) }
it 'returns user with updated role' do
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body)['group']).to eq('vip-1')
end
context 'invalid uid' do
let(:data) { { uid: 'fake_uid', group: 'vip-1' } }
it 'returns status 404 and error' do
request
expect(response).to have_http_status(404)
expect(JSON.parse(response.body)['error']).to eq("Couldn't find record.")
end
end
context 'invalid record' do
let(:data) { { uid: member.uid, group: 'vip-12222222222222222222222222222' } }
it 'returns status 422 and error' do
request
expect(response).to have_http_status(422)
expect(JSON.parse(response.body)['errors']).to eq("Validation failed: Group is too long (maximum is 32 characters)")
end
end
end
end

View File

@@ -0,0 +1,361 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Management::Operations, type: :request do
before do
defaults_for_management_api_v1_security_configuration!
management_api_v1_security_configuration.merge! \
scopes: {
read_operations: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
write_operations: { permitted_signers: %i[alex jeff james], mandatory_signers: %i[alex jeff] }
}
end
describe 'list operations' do
def request(op_type)
post_json "/api/v2/management/#{op_type}", multisig_jwt_management_api_v1({ data: data }, *signers)
end
def optional_request(op_type, data)
post_json "/api/v2/management/#{op_type}", multisig_jwt_management_api_v1({ data: data }, *signers)
end
Operations::Account::PLATFORM_TYPES.each do |op_type|
context op_type do
let(:data) { {} }
let(:signers) { %i[alex] }
let(:operations_number) { 15 }
let!(:operations) { create_list(op_type, operations_number) }
before do
request(op_type.to_s.pluralize)
end
it { expect(response).to have_http_status(200) }
context 'filter by currency' do
let(:data) { { currency: :btc } }
it { expect(response).to have_http_status(200) }
it 'returns operations by currency' do
operations = "operations/#{op_type}"
.camelize
.constantize
.where(currency_id: :btc)
expect(JSON.parse(response.body).count).to eq operations.count
expect(JSON.parse(response.body).map { |h| h['currency'] }).to\
eq operations.pluck(:currency_id)
end
end
context 'pagination' do
let(:data) { { page: 2, limit: 8 } }
it 'returns second page of operations' do
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).count).to eq 7
credits = "operations/#{op_type}"
.camelize
.constantize
.order(id: :desc)
.pluck(:credit)
# Consider that credit sequence is unique.
expect(JSON.parse(response.body).map { |h| h['credit'].to_d }).to eq credits[8..15]
end
end
end
end
Operations::Account::MEMBER_TYPES.each do |op_type|
context op_type do
let(:data) { {} }
let(:signers) { %i[alex] }
let(:operations_number) { 15 }
let!(:operations) { create_list(op_type, operations_number, :with_member) }
before do
request(op_type.to_s.pluralize)
end
it { expect(response).to have_http_status(200) }
context 'filter by currency' do
let(:data) { { currency: :btc } }
it 'returns operations by currency' do
expect(response).to have_http_status(200)
operations = "operations/#{op_type}"
.camelize
.constantize
.where(currency_id: :btc)
expect(JSON.parse(response.body).count).to eq operations.count
expect(JSON.parse(response.body).map { |h| h['currency'] }).to\
eq operations.pluck(:currency_id)
end
end
context 'filter by uid' do
let(:member) { create(:member, :barong) }
let!(:member_operations) do
create_list(op_type, operations_number, member_id: member.id)
end
let(:data) { { uid: member.uid } }
it 'returns operations by member UID' do
expect(response).to have_http_status(200)
request(op_type.to_s.pluralize)
operations = "operations/#{op_type}"
.camelize
.constantize
.where(member: member)
expect(JSON.parse(response.body).count).to eq operations.count
expect(JSON.parse(response.body).map { |h| h['uid'] }).to\
eq [member.uid] * operations_number
end
end
context 'filter by reference type' do
let(:deposit_data) { { reference_type: 'deposit' } }
let(:trade_data) { { reference_type: 'trade' } }
let(:order_data) { { reference_type: 'order' } }
it { expect(response).to have_http_status(200) }
def equal_amount!(response, optional_field, op_type)
operations = "operations/#{op_type}"
.camelize
.constantize
.where(optional_field)
expect(JSON.parse(response.body).count).to eq operations.count
end
it 'returns operations by reference type deposit' do
optional_request(op_type.to_s.pluralize, deposit_data)
equal_amount!(response, deposit_data, op_type)
end
it 'returns operations by reference type trade' do
optional_request(op_type.to_s.pluralize, trade_data)
equal_amount!(response, trade_data, op_type)
end
it 'returns operations by reference type order' do
optional_request(op_type.to_s.pluralize, order_data)
equal_amount!(response, order_data, op_type)
end
end
context 'time range' do
let(:time_from) { 2.days.ago }
let(:time_to) { 1.day.ago }
let(:data) { { time_from: time_from.to_i, time_to: time_to.to_i } }
it { expect(response).to have_http_status(200) }
it 'returns operations between 48h and 24h ago' do
request(op_type.to_s.pluralize)
operations = "operations/#{op_type}"
.camelize
.constantize
.where('created_at >= ?', time_from)
.where('created_at < ?', time_to)
expect(JSON.parse(response.body).count).to eq operations.count
end
end
context 'pagination' do
let(:data) { { page: 2, limit: 8 } }
it { expect(response).to have_http_status(200) }
it 'returns second page of operations' do
expect(JSON.parse(response.body).count).to eq 7
credits = "operations/#{op_type}"
.camelize
.constantize
.order(id: :desc)
.pluck(:credit)
# Consider that credit sequence is unique.
expect(JSON.parse(response.body).map{ |h| h['credit'].to_d }).to eq credits[8..15]
end
end
end
end
end
describe 'create operation' do
def request(op_type)
post_json "/api/v2/management/#{op_type}/new", multisig_jwt_management_api_v1({ data: data }, *signers)
end
Operations::Account::PLATFORM_TYPES.each do |op_type|
context op_type do
let(:currency) { Currency.coins.sample }
let(:signers) { %i[alex jeff] }
let(:data) do
{ currency: currency.code,
code: Operations::Account.find_by(type: op_type, currency_type: currency.type).code}
end
context 'credit' do
let(:amount) { '0.2515' }
before do
data[:credit] = amount
request(op_type.to_s.pluralize)
end
it 'returns operation' do
expect(response).to have_http_status 200
expect(JSON.parse(response.body)['currency']).to eq currency.code.to_s
expect(JSON.parse(response.body)['credit'].to_d).to eq amount.to_d
expect(JSON.parse(response.body)['code']).to \
eq Operations::Account.find_by(type: op_type,
kind: :main,
currency_type: currency.type).code
end
it 'saves operation' do
op_klass = "operations/#{op_type}"
.camelize
.constantize
expect { request(op_type.to_s.pluralize) }.to \
change(op_klass, :count).by(1)
end
context 'wrong account code' do
before do
data[:code] = (::Operations::Account.pluck(:code) - [data[:code]]).sample
request(op_type.to_s.pluralize)
end
it { expect(response).to have_http_status 422 }
end
end
context 'debit' do
let(:amount) { '0.1545' }
before do
# Create credit operation to avoid negative balance.
create(op_type, credit: amount)
data[:debit] = amount
request(op_type.to_s.pluralize)
end
it 'returns operation' do
expect(response).to have_http_status 200
expect(JSON.parse(response.body)['currency']).to eq currency.code.to_s
expect(JSON.parse(response.body)['debit'].to_d).to eq amount.to_d
expect(JSON.parse(response.body)['code']).to \
eq Operations::Account.find_by(type: op_type,
kind: :main,
currency_type: currency.type).code
end
it 'saves operation' do
op_klass = "operations/#{op_type}"
.camelize
.constantize
expect { request(op_type.to_s.pluralize) }.to \
change(op_klass, :count).by(1)
end
end
end
end
Operations::Account::MEMBER_TYPES.each do |op_type|
context op_type do
let(:currency) { Currency.coins.sample }
let(:signers) { %i[alex jeff] }
let(:member) { create(:member, :barong) }
let(:data) do
{ currency: currency.code,
code: Operations::Account.find_by(type: op_type, currency_type: currency.type, kind: :main).code,
uid: member.uid }
end
context 'credit' do
let(:amount) { '0.2515' }
before do
data[:credit] = amount
request(op_type.to_s.pluralize)
end
it 'returns operation' do
expect(response).to have_http_status 200
expect(JSON.parse(response.body)['uid']).to eq member.uid
expect(JSON.parse(response.body)['currency']).to eq currency.code.to_s
expect(JSON.parse(response.body)['credit'].to_d).to eq amount.to_d
expect(JSON.parse(response.body)['code']).to \
eq Operations::Account.find_by(type: op_type,
kind: :main,
currency_type: currency.type).code
end
it 'saves operation' do
op_klass = "operations/#{op_type}"
.camelize
.constantize
expect { request(op_type.to_s.pluralize) }.to \
change(op_klass, :count).by(1)
end
it 'updates legacy balance' do
currency_id = JSON.parse(response.body)['currency']
expect(member.get_account(currency_id).balance).to \
eq JSON.parse(response.body)['credit'].to_d
end
context 'wrong account code' do
before do
data[:code] = 999
request(op_type.to_s.pluralize)
end
it { expect(response).to have_http_status 422 }
end
end
context 'debit' do
let(:amount) { '0.1545' }
before do
# Create credit operation to avoid negative balance.
create(op_type, :with_member, credit: amount,
member: member, currency: currency)
data[:debit] = amount
request(op_type.to_s.pluralize)
end
it 'returns operation' do
expect(response).to have_http_status 200
expect(JSON.parse(response.body)['uid']).to eq member.uid
expect(JSON.parse(response.body)['currency']).to eq currency.code.to_s
expect(JSON.parse(response.body)['debit'].to_d).to eq amount.to_d
expect(JSON.parse(response.body)['code']).to \
eq Operations::Account.find_by(type: op_type,
kind: :main,
currency_type: currency.type).code
end
it 'saves operation' do
# Create one more credit operation to avoid negative balance.
# So we can create one more debit operation.
create(op_type, :with_member, credit: amount, member: member, currency: currency)
op_klass = "operations/#{op_type}"
.camelize
.constantize
expect { request(op_type.to_s.pluralize) }.to \
change(op_klass, :count).by(1)
end
it 'updates legacy balance' do
currency_id = JSON.parse(response.body)['currency']
expect(member.get_account(currency_id).balance).to \
eq 0.to_d
end
end
end
end
end
end

View File

@@ -0,0 +1,237 @@
# frozen_string_literal: true
describe API::V2::Management::Orders, type: :request do
let(:member1) { create(:member, :level_3) }
let(:member2) { create(:member, :level_3) }
let(:signers) { %i[alex jeff] }
let!(:finex_engine) { create(:engine, driver: 'finex-spot') }
before do
defaults_for_management_api_v1_security_configuration!
management_api_v1_security_configuration.merge! \
scopes: {
read_orders: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
write_orders: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }
}
Market.find('btceth').update!(engine: finex_engine)
end
describe 'POST /api/v2/management/orders' do
before do
create(:order_bid, :btcusd, member: member1, state: Order::CANCEL)
create(:order_ask, :btcusd, member: member1, state: Order::WAIT)
create(:order_ask, :btceth, member: member1, state: Order::DONE)
create(:order_bid, :btcusd, member: member2, state: Order::CANCEL)
create(:order_ask, :btcusd, member: member2, state: Order::WAIT)
create(:order_ask, :btceth, member: member2, state: Order::DONE)
end
def request
post_json '/api/v2/management/orders', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:data) { {} }
it 'returns all orders on the platform' do
request
expect(response).to have_http_status 200
expect(response_body.count).to eq(Order.count)
end
context 'by member' do
let(:data) do
{
uid: member1.uid
}
end
it 'returns only member orders' do
request
expect(response).to have_http_status 200
expect(response_body.pluck('member_id').uniq).to eq([member1.id])
end
end
context 'by member, market, state and order type' do
let(:data) do
{
uid: member1.uid,
market: 'btcusd',
state: 'wait',
ord_type: 'limit'
}
end
it 'returns only member orders on specific market with specific state and order type' do
request
expect(response).to have_http_status 200
expect(response_body.pluck('member_id').uniq).to eq([member1.id])
expect(response_body.pluck('state').uniq).to eq(['wait'])
expect(response_body.pluck('market').uniq).to eq(['btcusd'])
expect(response_body.pluck('ord_type').uniq).to eq(['limit'])
end
end
context 'invalid params' do
context 'member_uid' do
it 'returns status 422 and error' do
data[:uid] = 'invalid_uid'
request
expect(response).to have_http_status(422)
end
end
context 'market' do
it 'returns status 422 and error' do
data[:market] = 'invalid_market'
request
expect(response).to have_http_status(422)
end
end
context 'state' do
it 'returns status 422 and error' do
data[:state] = 'invalid_state'
request
expect(response).to have_http_status(422)
end
end
context 'ord_type' do
it 'returns status 422 and error' do
data[:ord_type] = 'invalid_ord_type'
request
expect(response).to have_http_status(422)
end
end
end
end
describe 'POST /api/v2/management/orders/:id/cancel' do
let!(:peatio_order) { create(:order_ask, :btcusd, member: member1, state: Order::WAIT) }
let!(:third_party_order) { create(:order_ask, :btceth, member: member1, state: Order::WAIT) }
def request(order_id)
post_json "/api/v2/management/orders/#{order_id}/cancel", multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:data) { {} }
it 'cancels an order on peatio market' do
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: peatio_order.to_matching_attributes)
AMQP::Queue.expects(:publish).with(finex_engine.driver, data: peatio_order.as_json_for_third_party, type: 3).never
request(peatio_order.id)
expect(response).to have_http_status 200
end
context 'third party order cancel' do
it 'cancel an order on third party market' do
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: third_party_order.to_matching_attributes).never
AMQP::Queue.expects(:publish).with(finex_engine.driver, data: third_party_order.as_json_for_third_party, type: 3)
request(third_party_order.id)
expect(response).to have_http_status 200
end
end
context 'invalid params' do
it 'returns status 404 and error' do
request(0)
expect(response).to have_http_status(404)
end
end
end
describe 'POST /api/v2/management/orders/cancel' do
let!(:member1_peatio_order) { create(:order_ask, :btcusd, member: member1, state: Order::WAIT) }
let!(:member2_peatio_order) { create(:order_ask, :btcusd, member: member2, state: Order::WAIT) }
let!(:member1_third_party_order) { create(:order_ask, :btceth, member: member1, state: Order::WAIT) }
let!(:member2_third_party_order) { create(:order_ask, :btceth, member: member2, state: Order::WAIT) }
def request
post_json '/api/v2/management/orders/cancel', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:data) { {} }
context 'peatio order cancel' do
it 'cancels the orders on peatio market' do
data[:market] = 'btcusd'
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: member1_peatio_order.to_matching_attributes)
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: member2_peatio_order.to_matching_attributes)
AMQP::Queue.expects(:publish).with(finex_engine.driver, data: { market_id: 'btcusd' }, type: 4).never
request
expect(response).to have_http_status 204
end
it 'cancels the orders on peatio market' do
data[:market] = 'btcusd'
data[:uid] = member1.uid
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: member1_peatio_order.to_matching_attributes)
AMQP::Queue.expects(:publish).with(finex_engine.driver, data: { market_id: 'btcusd' }, type: 4).never
request
expect(response).to have_http_status 204
end
end
context 'third party orders cancel' do
it 'cancels the orders on third party market' do
data[:market] = 'btceth'
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: member1_peatio_order.to_matching_attributes).never
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: member2_peatio_order.to_matching_attributes).never
AMQP::Queue.expects(:publish).with(finex_engine.driver, data: { market_id: 'btceth' }, type: 4)
request
expect(response).to have_http_status 204
end
it 'cancels the orders on third party market' do
data[:market] = 'btceth'
data[:uid] = member1.uid
AMQP::Queue.expects(:enqueue).with(:matching, action: 'cancel', order: member1_peatio_order.to_matching_attributes).never
AMQP::Queue.expects(:publish).with(finex_engine.driver, data: { market_id: 'btceth', member_uid: member1.uid }, type: 4)
request
expect(response).to have_http_status 204
end
end
context 'invalid params' do
context 'invalid market' do
it 'returns status 422 and error' do
data[:market] = 'btcbtc'
request
expect(response).to have_http_status(422)
end
end
context 'invalid uid' do
it 'returns status 422 and error' do
data[:market] = 'btceth'
data[:uid] = 'invalid_uid'
request
expect(response).to have_http_status(422)
end
end
end
end
end

View File

@@ -0,0 +1,113 @@
# frozen_string_literal: true
describe API::V2::Management::PaymentAddress, type: :request do
let(:member1) { create(:member, :level_3) }
let(:member2) { create(:member, :level_3) }
let(:signers) { %i[alex jeff] }
before do
defaults_for_management_api_v1_security_configuration!
management_api_v1_security_configuration.merge! \
scopes: {
write_payment_addresses: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
}
end
describe 'POST /api/v2/management/deposit_address/new' do
let(:data) { { currency: 'eth', uid: member1.uid } }
let(:address) { 'qwerty' }
def request
post_json '/api/v2/management/deposit_address/new', multisig_jwt_management_api_v1({ data: data }, *signers)
end
before do
WalletService.any_instance.stubs(:create_address!).returns({ address: address, secret: 'qwerty' })
end
it 'generates new address' do
request
expect(response_body).to eq({"address"=>address, "currencies"=>[data[:currency]], "remote"=>false, "state"=>"active", "uid"=>data[:uid]})
expect(response).to have_http_status 200
end
context 'generates new address for btc' do
it do
data[:currency] = 'btc'
request
expect(response_body).to eq({"address"=>address, "currencies"=>[data[:currency]], "remote"=>false, "state"=>"active", "uid"=>data[:uid]})
expect(response).to have_http_status 200
end
end
context 'generates new address with specified remote value' do
it do
data[:remote] = true
request
expect(response_body).to eq({"address"=>address, "currencies"=>[data[:currency]], "remote"=>true, "state"=>"active", "uid"=>data[:uid]})
expect(response).to have_http_status 200
end
end
context 'missing required params' do
context 'uid' do
it do
data.delete(:uid)
request
expect(response.status).to eq 422
expect(response.body).to match(/uid is missing/i)
end
end
context 'currency' do
it do
data.delete(:currency)
request
expect(response.status).to eq 422
expect(response.body).to match(/currency is missing/i)
end
end
end
context 'non-existing params applied' do
context 'uid' do
it do
data[:uid] = '123456'
request
expect(response.status).to eq 422
expect(response.body).to match(/management.payment_address.uid_doesnt_exist/i)
end
end
context 'currency' do
it do
data[:currency] = 'uah'
request
expect(response.status).to eq 422
expect(response.body).to match(/management.payment_address.currency_doesnt_exist/i)
end
end
context 'remote' do
it do
data[:remote] = 'remote'
request
expect(response.status).to eq 422
expect(response.body).to match(/management.payment_address.non_boolean_remote/i)
end
end
end
context 'wallet service raised an error' do
before do
WalletService.any_instance.stubs(:create_address!).raises(StandardError.new)
end
it do
request
expect(response.body).to match(/management.payment_address.failed_to_generate/i)
expect(response).to have_http_status 422
end
end
end
end

View File

@@ -0,0 +1,29 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Management::Tools, type: :request do
before do
defaults_for_management_api_v1_security_configuration!
management_api_v1_security_configuration.merge! \
scopes: {
tools: { permitted_signers: %i[alex jeff], mandatory_signers: %i[jeff] }
}
end
describe 'management/timestamp' do
let(:data) { {} }
let(:signers) { %i[jeff] }
def request
post_json '/api/v2/management/timestamp',
multisig_jwt_management_api_v1({ data: data }, *signers)
end
it 'returns current time in seconds' do
now = Time.now
request
expect(response).to be_successful
expect(JSON.parse(response.body).fetch('timestamp')).to be_between(now.iso8601, (now + 1).iso8601)
end
end
end

View File

@@ -0,0 +1,102 @@
describe API::V2::Management::Trades, type: :request do
let(:member) do
create(:member, :level_3).tap do |m|
m.get_account(:btc).update_attributes(balance: 12.13, locked: 3.14)
m.get_account(:usd).update_attributes(balance: 2014.47, locked: 0)
end
end
let(:second_member) do
create(:member, :level_3).tap do |m|
m.get_account(:btc).update_attributes(balance: 12.13, locked: 3.14)
m.get_account(:usd).update_attributes(balance: 2014.47, locked: 0)
end
end
let(:btcusd_ask) do
create(
:order_ask,
:btcusd,
price: '12.32'.to_d,
volume: '123.12345678',
member: member
)
end
let(:btceth_ask) do
create(
:order_ask,
:btceth,
price: '12.326'.to_d,
volume: '123.1234',
member: second_member
)
end
let(:btcusd_bid) do
create(
:order_bid,
:btcusd,
price: '12.32'.to_d,
volume: '123.12345678',
member: member
)
end
let(:btceth_bid) do
create(
:order_bid,
:btceth,
price: '12.326'.to_d,
volume: '123.1234',
member: second_member
)
end
let!(:btcusd_ask_trade) { create(:trade, :btcusd, maker_order: btcusd_ask, created_at: 2.days.ago) }
let!(:btceth_ask_trade) { create(:trade, :btceth, maker_order: btceth_ask, created_at: 2.days.ago) }
let!(:btcusd_bid_trade) { create(:trade, :btcusd, taker_order: btcusd_bid, created_at: 23.hours.ago) }
let!(:btceth_bid_trade) { create(:trade, :btceth, taker_order: btceth_bid, created_at: 23.hours.ago) }
before do
defaults_for_management_api_v1_security_configuration!
management_api_v1_security_configuration.merge! \
scopes: {
read_trades: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
}
end
def request
post_json '/api/v2/management/trades', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:data) { {} }
let(:signers) { %i[alex jeff] }
it 'returns all recent trades' do
request
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.count).to eq 4
end
it 'returns trades by uid of user' do
data.merge!(uid: member.uid)
request
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.count).to eq 2
end
it 'returns trades by market' do
data.merge!(market: 'btcusd')
request
expect(response).to be_successful
result = JSON.parse(response.body)
expect(result.count).to eq 2
end
end

View File

@@ -0,0 +1,72 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Management::TradingFees, type: :request do
before do
defaults_for_management_api_v1_security_configuration!
management_api_v1_security_configuration.merge! \
scopes: {
read_trading_fees: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
}
end
describe '/fee_schedule/trading_fees' do
def request
post_json '/api/v2/management/fee_schedule/trading_fees', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:data) {}
let(:signers) { %i[alex jeff] }
before do
create(:trading_fee, maker: 0.0005, taker: 0.001, market_id: :btcusd, group: 'vip-0')
create(:trading_fee, maker: 0.0008, taker: 0.001, market_id: :any, group: 'vip-0')
create(:trading_fee, maker: 0.001, taker: 0.0012, market_id: :btcusd, group: :any)
end
it 'returns all trading fees tables' do
request
expect(response).to have_http_status(200)
expect(response.headers.fetch('Total')).to eq('4')
end
context 'group: vip-0, market: btcusd' do
let(:data) { { group: 'vip-0', market_id: 'btcusd' } }
it 'returns trading fee with btcusd market_id and vip-0 group' do
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).first['maker']).to eq('0.0005')
expect(JSON.parse(response.body).first['taker']).to eq('0.001')
expect(JSON.parse(response.body).first['group']).to eq('vip-0')
expect(JSON.parse(response.body).first['market_id']).to eq('btcusd')
expect(response.headers.fetch('Total')).to eq('1')
end
end
context 'group: any, market: btcusd' do
let(:data) { { group: 'any', market_id: 'btcusd' } }
it 'returns trading fee with btcusd market_id and `any` group' do
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).first['maker']).to eq('0.001')
expect(JSON.parse(response.body).first['taker']).to eq('0.0012')
expect(JSON.parse(response.body).first['group']).to eq('any')
expect(JSON.parse(response.body).first['market_id']).to eq('btcusd')
expect(response.headers.fetch('Total')).to eq('1')
end
end
context 'group: vip-0, market: any' do
let(:data) { { group: 'vip-0', market_id: 'any' } }
it 'returns trading fee with btcusd market_id and `any` group' do
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).first['maker']).to eq('0.0008')
expect(JSON.parse(response.body).first['taker']).to eq('0.001')
expect(JSON.parse(response.body).first['group']).to eq('vip-0')
expect(JSON.parse(response.body).first['market_id']).to eq('any')
expect(response.headers.fetch('Total')).to eq('1')
end
end
end
end

View File

@@ -0,0 +1,485 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Management::Transfers, type: :request do
before do
defaults_for_management_api_v1_security_configuration!
management_api_v1_security_configuration.merge! \
scopes: {
read_transfers: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
write_transfers: { permitted_signers: %i[alex jeff james], mandatory_signers: %i[alex jeff] }
}
end
describe 'create operation' do
def request
post_json '/api/v2/management/transfers/new', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:currency) { Currency.coins.sample }
let(:signers) { %i[alex jeff] }
let(:data) do
{ key: generate(:transfer_key),
category: Transfer::CATEGORIES.sample,
description: "Referral program payoffs (#{Time.now.to_date})",
operations: operations }
end
let(:valid_operation) do
{ currency: :btc,
amount: '0.0001',
account_src: {
code: 102
},
account_dst: {
code: 102
} }
end
context 'automatically creates an account, if transfer will be send' do
let!(:sender_member) { create(:member, :level_3) }
let!(:receiver_member) { create(:member, :level_3) }
let!(:deposit) { create(:deposit_btc, member: sender_member, amount: 1) }
let(:operation) do
{
currency: :btc,
amount: '0.5',
account_src: {
code: 202,
uid: sender_member.uid
},
account_dst: {
code: 202,
uid: receiver_member.uid
}
}
end
let(:operations) { [operation] }
before do
deposit.accept!
deposit.process!
deposit.dispatch!
end
it do
expect(receiver_member.accounts.count).to eq(0)
request
expect(response).to have_http_status(201)
expect(receiver_member.accounts.count).to eq(1)
end
end
context 'empty key' do
let(:operations) { [valid_operation] }
before do
data.delete(:key)
request
end
it do
expect(response).to have_http_status(422)
expect(response.body).to match(/key is missing/i)
end
end
context 'empty category' do
let(:operations) {[valid_operation]}
before do
data.delete(:category)
request
end
it do
expect(response).to have_http_status(422)
expect(response.body).to match(/category is missing/i)
end
end
context 'empty description' do
let(:operations) {[valid_operation]}
before do
data.delete(:description)
request
end
it { expect(response).to have_http_status(201) }
end
context 'empty operations' do
let(:operations) {[]}
before { request }
it do
expect(response).to have_http_status(422)
expect(response.body).to match(/operations is empty/i)
end
end
context 'invalid account code' do
let(:operations) do
valid_operation[:account_src][:code] = 999
[valid_operation]
end
before { request }
it do
expect(response).to have_http_status(422)
expect(response.body).to match(/does not have a valid value/i)
end
end
context 'invalid currency' do
let(:operations) do
valid_operation[:currency] = :neo
[valid_operation]
end
before { request }
it do
expect(response).to have_http_status(422)
expect(response.body).to match(/does not have a valid value/i)
end
end
context 'invalid amount' do
let(:operations) do
valid_operation[:amount] = -1
[valid_operation]
end
before { request }
it do
expect(response).to have_http_status(422)
expect(response.body).to match(/does not have a valid value/i)
end
end
context 'existing transfer key' do
let(:operations) {[valid_operation]}
before do
t = create(:transfer)
data[:key] = t.key
request
end
it do
expect(response).to have_http_status 422
expect(response.body).to match(/key has already been taken/i)
end
end
context 'debit liability on account with insufficient balance' do
let!(:sender_member) { create(:member, :level_3) }
let!(:receiver_member) { create(:member, :level_3) }
let!(:deposit) { create(:deposit_btc, member: sender_member, amount: 1) }
let(:operation) do
{
currency: :btc,
amount: '1.1',
account_src: {
code: 202,
uid: sender_member.uid
},
account_dst: {
code: 202,
uid: receiver_member.uid
}
}
end
let(:operations) {[operation]}
before { request }
it do
expect(response).to have_http_status 422
expect(response.body).to match(/account balance is insufficient/i)
end
end
context 'referral program story' do
# In case of referral program some fees received by platform
# during trading are returned to the referrer once per 4h for example.
# We debit Revenue account balance and credit member Liabilities.
before do
# Credit Revenue accounts.
create(:revenue, currency_id: base_unit,
code: coin_revenues_code, credit: 10)
create(:revenue, currency_id: quote_unit,
code: fiat_revenues_code, credit: 100)
end
let(:referrer1) { create(:member, :barong) }
let(:referrer2) { create(:member, :barong) }
let(:referrer3) { create(:member, :barong) }
let(:base_unit) { Currency.coins.ids.sample }
let(:quote_unit) { Currency.fiats.ids.sample }
let(:coin_liabilities_code) { 202 }
let(:fiat_liabilities_code) { 201 }
let(:coin_revenues_code) { 302 }
let(:fiat_revenues_code) { 301 }
# Consider we have BTC/USD market
# Return all BTC/USD fees for 4h in single batch.
# Balance changes:
# Liability:
# referrer1:
# base_unit: 0.0001 + 0.0003
# quote_unit: 0
# referrer2:
# base_unit: 0.00015
# quote_unit: 0.05
# referrer3:
# base_unit: 0
# quote_unit: 0.075
# Revenue:
# base_unit: -(0.0001 + 0.0003 + 0.00015)
# quote_unit -(0.05 + 0.075)
let(:operations) do
[
{
currency: base_unit,
amount: '0.0001',
account_src: {
code: coin_revenues_code
},
account_dst: {
code: coin_liabilities_code,
uid: referrer1.uid
}
},
{
currency: base_unit,
amount: '0.00015',
account_src: {
code: coin_revenues_code
},
account_dst: {
code: coin_liabilities_code,
uid: referrer2.uid
}
},
{
currency: base_unit,
amount: '0.0003',
account_src: {
code: coin_revenues_code
},
account_dst: {
code: coin_liabilities_code,
uid: referrer1.uid
}
},
{
currency: quote_unit,
amount: '0.075',
account_src: {
code: fiat_revenues_code
},
account_dst: {
code: fiat_liabilities_code,
uid: referrer3.uid
}
},
{
currency: quote_unit,
amount: '0.05',
account_src: {
code: fiat_revenues_code
},
account_dst: {
code: fiat_liabilities_code,
uid: referrer2.uid
}
}
]
end
it do
request
expect(response).to have_http_status 201
end
it 'returns transfer with operations' do
request
expect(JSON.parse(response.body)['key']).to eq data[:key]
expect(JSON.parse(response.body)['category']).to eq data[:category]
expect(JSON.parse(response.body)['description']).to eq data[:description]
expect(JSON.parse(response.body)['liabilities'].size).to eq operations.size
expect(JSON.parse(response.body)['revenues'].size).to eq operations.size
end
it 'saves liabilities' do
expect { request }.to change(::Operations::Liability, :count).by(operations.size)
end
it 'saves revenues' do
expect { request }.to change(::Operations::Revenue, :count).by(operations.size)
end
it 'updates legacy balances' do
expect { request }.to change{ referrer1.get_account(base_unit).balance }.by(0.0001 + 0.0003).and \
change{ referrer2.get_account(base_unit).balance }.by(0.00015).and \
change{ referrer2.get_account(quote_unit).balance }.by(0.05).and \
change{ referrer3.get_account(quote_unit).balance }.by(0.075)
end
context 'wrong account code' do
let(:operations) do
[
{
currency: base_unit,
amount: '0.0001',
account_src: {
code: fiat_revenues_code # Wrong code because base_unit is coin.
},
account_dst: {
code: coin_liabilities_code,
uid: referrer2.uid
}
},
{
currency: quote_unit,
amount: '0.05',
account_src: {
code: fiat_revenues_code
},
account_dst: {
code: coin_liabilities_code, # Wrong code because quote_unit is fiat.
uid: referrer2.uid
}
}
]
end
it do
request
expect(response).to have_http_status 422
end
it 'doesn\'t save transfer' do
expect { request }.to_not change(Transfer, :count)
end
it 'doesn\'t save liabilities' do
expect { request }.to_not change(::Operations::Liability, :count)
end
it 'doesn\'t save revenues' do
expect { request }.to_not change(::Operations::Revenue, :count)
end
end
end
context 'token distribution story' do
# In token distribution story we credit member token balance
# once member is signed in for the first time.
before do
# Add token-distribution Liabilities account.
Rails.configuration.x.chart_of_accounts << coin_distribution_account
create(:operations_account, coin_distribution_account)
end
before do
# 1. Credit main Assets account.
# 2. Credit token-distribution Liabilities account.
# So we keep Balance Sheet equal Income Statement.
create(:asset, currency_id: coin,
code: coin_assets_code, credit: 1000)
create(:liability, currency_id: coin,
code: coin_distribution_account_code, credit: 1000, member_id: nil)
end
let(:member1) { create(:member, :barong) }
let(:member2) { create(:member, :barong) }
let(:coin) { :trst }
let(:coin_assets_code) { 102 }
let(:coin_liabilities_code) { 202 }
let(:coin_distribution_account_code) do
coin_distribution_account[:code]
end
let(:coin_distribution_account) do
{ code: 292,
type: :liability,
kind: 'token-distribution',
currency_type: :coin,
description: 'Token Distributions Liabilities Account',
scope: :platform
}
end
# Balance changes:
# Liability-main:
# member1:
# coin: 10
# member2:
# coin: 5
# Liability-token-distribution:
# coin: -15
let(:operations) do
[
{
currency: coin,
amount: 10,
account_src: {
code: coin_distribution_account_code
},
account_dst: {
code: coin_liabilities_code,
uid: member1.uid
}
},
{
currency: coin,
amount: 5,
account_src: {
code: coin_distribution_account_code
},
account_dst: {
code: coin_liabilities_code,
uid: member2.uid
}
}
]
end
it do
request
expect(response).to have_http_status 201
end
it 'returns transfer with liabilities' do
request
expect(JSON.parse(response.body)['key']).to eq data[:key]
expect(JSON.parse(response.body)['category']).to eq data[:category]
expect(JSON.parse(response.body)['description']).to eq data[:description]
# Two liability operation for each token-distribution operation.
expect(JSON.parse(response.body)['liabilities'].size).to eq operations.size * 2
end
it 'saves liabilities' do
expect { request }.to change(::Operations::Liability, :count).by(operations.size * 2)
end
it 'updates legacy balance' do
expect { request }.to change{ member1.get_account(coin).balance }.by(10).and \
change{ member2.get_account(coin).balance }.by(5)
end
end
end
end

View File

@@ -0,0 +1,468 @@
# encoding: UTF-8
# frozen_string_literal: true
describe API::V2::Management::Withdraws, type: :request do
before do
defaults_for_management_api_v1_security_configuration!
management_api_v1_security_configuration.merge! \
scopes: {
read_withdraws: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] },
write_withdraws: { permitted_signers: %i[alex jeff james], mandatory_signers: %i[alex jeff] }
}
end
describe 'list withdraws' do
def request
post_json '/api/v2/management/withdraws', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:data) { {} }
let(:signers) { %i[alex jeff] }
let(:members) { create_list(:member, 2, :barong) }
before do
Withdraw::STATES.tap do |states|
(states.count * 2).times do
create(:btc_withdraw, :with_deposit_liability, sum: 1, member: members.sample, aasm_state: states.sample, rid: Faker::Blockchain::Bitcoin.address)
create(:usd_withdraw, :with_deposit_liability, sum: 1, member: members.sample, aasm_state: states.sample, rid: Faker::Bank.iban)
end
end
end
it 'returns withdraws' do
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).map { |x| x.fetch('tid') }).to eq Withdraw.order(id: :desc).pluck(:tid)
end
it 'filters by member' do
member = members.last
data.merge!(uid: member.uid)
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).count).to eq member.withdraws.count
end
it 'filters by currency' do
data.merge!(currency: :usd)
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).count).to eq Withdraw.where(currency_id: :usd).count
end
it 'filters by state' do
Withdraw::STATES.each do |state|
data.merge!(state: state)
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).count).to eq Withdraw.where(aasm_state: state).count
end
end
it 'paginates' do
ids = Withdraw.order(id: :desc).pluck(:tid)
data.merge!(page: 1, limit: 4)
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).map { |x| x.fetch('tid') }).to eq ids[0...4]
data.merge!(page: 3, limit: 4)
request
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).map { |x| x.fetch('tid') }).to eq ids[8...12]
end
end
describe 'create withdraw' do
def request
post_json '/api/v2/management/withdraws/new', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:member) { create(:member, :barong) }
let(:currency) { Currency.find(:btc) }
let(:amount) { 0.1575 }
let(:signers) { %i[alex jeff] }
let :data do
{ uid: member.uid,
currency: currency.code,
amount: amount.to_s,
rid: Faker::Blockchain::Bitcoin.address }
end
let(:account) { member.get_account(currency) }
let(:balance) { 1.2 }
before { account.plus_funds(balance) }
context 'crypto withdraw' do
it 'creates new withdraw and immediately submits it' do
request
expect(response).to have_http_status(201)
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
expect(record.sum).to eq 0.1575
expect(record.aasm_state).to eq 'accepted'
expect(record.rid).to eq data[:rid]
expect(record.account).to eq account
expect(record.account.balance).to eq (1.2 - amount)
expect(record.account.locked).to eq amount
expect(response_body['transfer_type']).to eq 'crypto'
end
context 'disabled currency' do
before do
currency.update(withdrawal_enabled: false)
end
it 'returns error for disabled withdrawal' do
request
expect(response).to have_http_status(422)
expect(response).to include_api_error('management.currency.withdrawal_disabled')
end
end
context 'withdrawal with beneficiary' do
let(:beneficiary) { create(:beneficiary, state: :active, currency: currency) }
let(:data) do
{ uid: member.uid,
currency: currency.code,
amount: amount.to_s,
beneficiary_id: beneficiary.id }
end
it 'creates new withdraw and immediately submits it' do
request
expect(response).to have_http_status(201)
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
expect(record.sum).to eq 0.1575
expect(record.aasm_state).to eq 'accepted'
expect(record.rid).to eq beneficiary.rid
expect(record.account).to eq account
expect(record.account.balance).to eq (1.2 - amount)
expect(record.account.locked).to eq amount
end
context 'pending beneficiary' do
before do
beneficiary.update(state: :pending)
end
it 'returns error for pending beneficiary' do
request
expect(response).to have_http_status(422)
expect(response).to include_api_error('management.beneficiary.invalid_state_for_withdrawal')
end
end
context 'withdrawal with note' do
let(:member) { create(:member, :barong) }
let(:currency) { Currency.find(:btc) }
let(:amount) { 0.1575 }
let(:signers) { %i[alex jeff] }
let :data do
{ uid: member.uid,
currency: currency.code,
amount: amount.to_s,
rid: Faker::Blockchain::Bitcoin.address,
note: 'Withdraw money'
}
end
it 'returns new withdraw with correct note' do
request
expect(JSON.parse(response.body)['note']).to eq 'Withdraw money'
end
end
context 'withdrawal with transfer_type' do
let(:member) { create(:member, :barong) }
let(:currency) { Currency.find(:btc) }
let(:amount) { 0.1575 }
let(:signers) { %i[alex jeff] }
let :data do
{ uid: member.uid,
currency: currency.code,
amount: amount.to_s,
rid: Faker::Blockchain::Bitcoin.address,
transfer_type: 'card'
}
end
it 'returns new withdraw with correct transfer_type' do
request
expect(JSON.parse(response.body)['transfer_type']).to eq 'card'
end
end
end
context 'action: :process' do
it 'creates new withdraw and immediately submits it' do
data.merge!(action: 'process')
request
expect(response).to have_http_status(201)
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
expect(record.sum).to eq 0.1575
expect(record.aasm_state).to eq 'accepted'
expect(record.rid).to eq data[:rid]
expect(record.account).to eq account
expect(record.account.balance).to eq (1.2 - amount)
expect(record.account.locked).to eq amount
end
end
context 'invalid withdraw' do
before do
data[:action] = :process
data[:amount] = '1000'
end
it 'validates enough balance' do
request
expect(response).to have_http_status(422)
expect(JSON.parse(response.body)).to eq("errors"=>["Account balance is insufficient"])
end
end
end
context 'extremely precise values' do
before { Currency.any_instance.stubs(:withdraw_fee).returns(BigDecimal(0)) }
before { Currency.any_instance.stubs(:precision).returns(16) }
it 'keeps precision for amount' do
currency.update!(precision: 16)
data.merge!(amount: '0.0000000123456789')
request
expect(response).to have_http_status(201)
expect(Withdraw.last.sum.to_s).to eq data[:amount]
end
end
context 'fiat withdraw' do
let(:currency) { Currency.find(:usd) }
let(:amount) { 5 }
let(:balance) { 20 }
it 'creates new withdraw with state set to «submitted»' do
request
expect(response).to have_http_status(201)
expect(account.reload.balance).to eq(15)
expect(account.reload.locked).to eq 5
expect(Withdraw.last.aasm_state).to eq 'accepted'
end
context 'action: :process' do
it 'creates new withdraw with state set to «submitted»' do
data.merge!(action: :process)
request
expect(response).to have_http_status(201)
expect(account.reload.balance).to eq(15)
expect(account.reload.locked).to eq 0
expect(Withdraw.last.aasm_state).to eq 'succeed'
end
end
end
end
describe 'get withdraw' do
def request
post_json '/api/v2/management/withdraws/get', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:signers) { %i[alex jeff] }
let(:data) { { tid: record.tid } }
let(:record) { create(:btc_withdraw, :with_deposit_liability, member: member) }
let(:member) { create(:member, :barong) }
it 'returns withdraw by TID' do
request
expect(JSON.parse(response.body).fetch('tid')).to eq record.tid
end
end
describe 'update withdraw' do
def request
put_json '/api/v2/management/withdraws/action', multisig_jwt_management_api_v1({ data: data }, *signers)
end
let(:currency) { Currency.find(:usd) }
let(:member) { create(:member, :barong) }
let(:amount) { 160.79 }
let(:signers) { %i[alex jeff] }
let(:data) { { tid: record.tid } }
let(:account) { member.get_account(currency) }
let(:record) { "Withdraws::#{currency.type.camelize}".constantize.create!(member: member, sum: amount, rid: Faker::Bank.iban, currency: currency) }
let(:balance) { 800.77 }
before { account.plus_funds(balance) }
context 'crypto withdraws' do
let(:currency) { Currency.find(:btc) }
context 'action: :process' do
before { data[:action] = :process }
it 'processes prepared withdraws' do
expect(record.aasm_state).to eq 'prepared'
expect(account.reload.balance).to eq balance
expect(account.reload.locked).to eq 0
request
expect(response).to have_http_status(200)
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
expect(record.aasm_state).to eq 'accepted'
expect(record.account.balance).to eq (balance - amount)
expect(record.account.locked).to eq amount
end
it 'processes submitted withdraws' do
record.accept!
expect(record.aasm_state).to eq 'accepted'
expect(account.reload.balance).to eq (balance - amount)
expect(account.reload.locked).to eq amount
request
expect(response).to have_http_status(200)
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
expect(record.aasm_state).to eq 'accepted'
expect(record.account.balance).to eq (balance - amount)
expect(record.account.locked).to eq amount
end
it 'processes accepted withdraws' do
record.accept!
record.accept!
expect(record.aasm_state).to eq 'accepted'
expect(account.reload.balance).to eq (balance - amount)
expect(account.reload.locked).to eq amount
request
expect(response).to have_http_status(200)
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
expect(record.aasm_state).to eq 'accepted'
expect(record.account.balance).to eq (balance - amount)
expect(record.account.locked).to eq amount
end
end
context 'action: :cancel' do
before { data[:action] = :cancel }
it 'cancels prepared withdraws' do
expect(record.aasm_state).to eq 'prepared'
expect(account.reload.balance).to eq balance
expect(account.reload.locked).to eq 0
request
expect(response).to have_http_status(200)
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
expect(record.aasm_state).to eq 'canceled'
expect(record.account.balance).to eq balance
expect(record.account.locked).to eq 0
end
it 'cancels submitted withdraws' do
record.accept!
expect(record.aasm_state).to eq 'accepted'
expect(account.reload.balance).to eq (balance - amount)
expect(account.reload.locked).to eq amount
request
expect(response).to have_http_status(200)
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
expect(record.aasm_state).to eq 'canceled'
expect(record.account.balance).to eq balance
expect(record.account.locked).to eq 0
end
it 'cancels accepted withdraws' do
record.accept!
record.accept!
expect(record.aasm_state).to eq 'accepted'
expect(account.reload.balance).to eq (balance - amount)
expect(account.reload.locked).to eq amount
request
expect(response).to have_http_status(200)
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
expect(record.aasm_state).to eq 'canceled'
expect(record.account.balance).to eq balance
expect(record.account.locked).to eq 0
end
end
end
context 'fiat withdraws' do
context 'action: :process' do
before { data[:action] = :process }
it 'processes prepared withdraws' do
expect(record.aasm_state).to eq 'prepared'
expect(account.reload.balance).to eq balance
expect(account.reload.locked).to eq 0
request
expect(response).to have_http_status(200)
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
expect(record.aasm_state).to eq 'succeed'
expect(record.account.balance).to eq (balance - amount)
expect(record.account.locked).to eq 0
end
it 'processes accepted withdraws' do
record.accept!
expect(record.aasm_state).to eq 'accepted'
expect(account.reload.balance).to eq (balance - amount)
expect(account.reload.locked).to eq amount
request
expect(response).to have_http_status(200)
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
expect(record.aasm_state).to eq 'succeed'
expect(record.account.balance).to eq (balance - amount)
expect(record.account.locked).to eq 0
end
it 'processes accepted withdraws' do
record.accept!
expect(record.aasm_state).to eq 'accepted'
expect(account.reload.balance).to eq (balance - amount)
expect(account.reload.locked).to eq amount
request
expect(response).to have_http_status(200)
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
expect(record.aasm_state).to eq 'succeed'
expect(record.account.balance).to eq (balance - amount)
expect(record.account.locked).to eq 0
end
end
context 'action: :cancel' do
before { data[:action] = :cancel }
it 'cancels prepared withdraws' do
expect(record.aasm_state).to eq 'prepared'
expect(account.reload.balance).to eq balance
expect(account.reload.locked).to eq 0
request
expect(response).to have_http_status(200)
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
expect(record.aasm_state).to eq 'canceled'
expect(record.account.balance).to eq balance
expect(record.account.locked).to eq 0
end
it 'cancels accepted withdraws' do
record.accept!
expect(record.aasm_state).to eq 'accepted'
expect(account.reload.balance).to eq (balance - amount)
expect(account.reload.locked).to eq amount
request
expect(response).to have_http_status(200)
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
expect(record.aasm_state).to eq 'canceled'
expect(record.account.balance).to eq balance
expect(record.account.locked).to eq 0
end
it 'cancels accepted withdraws' do
record.accept!
record.accept!
expect(record.aasm_state).to eq 'accepted'
expect(account.reload.balance).to eq (balance - amount)
expect(account.reload.locked).to eq amount
request
expect(response).to have_http_status(200)
record = Withdraw.find_by_tid!(JSON.parse(response.body).fetch('tid'))
expect(record.aasm_state).to eq 'canceled'
expect(record.account.balance).to eq balance
expect(record.account.locked).to eq 0
end
end
end
end
end