diff --git a/.semver b/.semver new file mode 100644 index 0000000000..4ca2fcf050 --- /dev/null +++ b/.semver @@ -0,0 +1,6 @@ +--- +:major: 3 +:minor: 2 +:patch: 1 +:special: '' +:metadata: '' diff --git a/Gemfile b/Gemfile index 85dcec936f..38ae6e23c7 100644 --- a/Gemfile +++ b/Gemfile @@ -33,7 +33,11 @@ gem 'faraday_middleware', '~> 0.13.1' gem 'faye', '~> 1.4' gem 'eventmachine', '~> 1.2' gem 'em-synchrony', '~> 1.0' -gem 'jwt', '~> 2.2.0' + +# We use 2.3.0.dev for bitzlato client +# Fill free to update to rubygem version when it will be released +gem 'jwt', github: 'jwt/ruby-jwt' + gem 'email_validator', '~> 1.6.0' gem 'validate_url', '~> 1.0.4' gem 'god', '~> 0.13.7', require: false @@ -71,6 +75,10 @@ group :development do gem 'annotate', '~> 3.1.0' gem 'ruby-prof', '~> 0.17.0', require: false gem 'listen', '>= 3.0.5', '< 3.2' + + gem 'rspec' + gem 'guard' + gem 'guard-rspec', github: 'caspark/guard-rspec' # Use from github to remove rspec < 4.0 dependencies end group :test do @@ -90,3 +98,7 @@ Dir.glob File.expand_path('../Gemfile.plugin', __FILE__) do |file| end gem "pg", "~> 1.2" + +gem "http_accept_language", "~> 2.1" + +gem "semver2", "~> 3.4" diff --git a/Gemfile.lock b/Gemfile.lock index 8406de140d..49d18fe56f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,6 +6,30 @@ GIT rails (>= 4.1) vault (~> 0.14) +GIT + remote: https://github.com/caspark/guard-rspec + revision: 407ef8c62f3abad575bb66d53c02ba062a5ee8af + specs: + guard-rspec (4.7.3) + guard (~> 2.1) + guard-compat (~> 1.1) + +GIT + remote: https://github.com/finfex/bitzlato + revision: 53b9c814a0be82f7d39c36abc4b9f37770bff405 + branch: main + specs: + bitzlato (0.1.8) + faraday (>= 0.17) + json (~> 2.0) + jwt (~> 2.3.0.dev) + +GIT + remote: https://github.com/jwt/ruby-jwt + revision: 2cea14fdae439773fafc59640178e5cf7a0af8a4 + specs: + jwt (2.3.0.dev) + GEM remote: https://rubygems.org/ specs: @@ -164,6 +188,7 @@ GEM ffi (1.15.0) figaro (1.1.1) thor (~> 0.14) + formatador (0.2.5) globalid (0.4.2) activesupport (>= 4.2.0) god (0.13.7) @@ -189,9 +214,20 @@ GEM rack grape_on_rails_routes (0.3.2) rails (>= 3.1.1) + guard (2.16.2) + formatador (>= 0.2.4) + listen (>= 2.7, < 4.0) + lumberjack (>= 1.0.12, < 2.0) + nenv (~> 0.1) + notiffany (~> 0.0) + pry (>= 0.9.12) + shellany (~> 0.0) + thor (>= 0.18.1) + guard-compat (1.2.1) hashdiff (1.0.1) hashie (3.6.0) hiredis (0.6.3) + http_accept_language (2.1.1) http_parser.rb (0.6.0) i18n (1.8.10) concurrent-ruby (~> 1.0) @@ -206,7 +242,7 @@ GEM faraday_middleware (~> 0.13.1) faye (~> 1.2) peatio (>= 2.4.2) - jwt (2.2.2) + json (2.5.1) jwt-multisig (1.0.5) activesupport (>= 4.0) jwt (~> 2.2) @@ -232,6 +268,7 @@ GEM loofah (2.9.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) + lumberjack (1.2.8) mail (2.7.1) mini_mime (>= 0.1.1) marcel (0.3.3) @@ -254,12 +291,16 @@ GEM mustermann-grape (1.0.1) mustermann (>= 1.0.0) mysql2 (0.5.3) + nenv (0.3.0) net-http-persistent (3.0.1) connection_pool (~> 2.2) nio4r (2.5.7) nokogiri (1.11.2) mini_portile2 (~> 2.5.0) racc (~> 1.4) + notiffany (0.1.3) + nenv (~> 0.1) + shellany (~> 0.0) parallel (1.20.1) parser (3.0.0.0) ast (~> 2.4.1) @@ -380,6 +421,10 @@ GEM reline (0.2.4) io-console (~> 0.5) rexml (3.2.4) + rspec (3.9.0) + rspec-core (~> 3.9.0) + rspec-expectations (~> 3.9.0) + rspec-mocks (~> 3.9.0) rspec-core (3.9.0) rspec-support (~> 3.9.0) rspec-expectations (3.9.0) @@ -420,8 +465,10 @@ GEM safe_yaml (1.0.5) scout_apm (2.6.10) parser + semver2 (3.4.2) sentry-raven (2.9.0) faraday (>= 0.7.6, < 1.0) + shellany (0.0.1) sprockets (4.0.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -463,6 +510,7 @@ DEPENDENCIES annotate (~> 3.1.0) arel-is-blank (~> 1.0.0) better-faraday (~> 1.0.5) + bitzlato! bootsnap (>= 1.1.0) bullet (~> 5.9) bump (~> 0.7) @@ -490,12 +538,15 @@ DEPENDENCIES grape-swagger-ui (~> 2.2.8) grape_logging (~> 1.8.0) grape_on_rails_routes (~> 0.3.2) + guard + guard-rspec! hashie (~> 3.6.0) hiredis (~> 0.6.0) + http_accept_language (~> 2.1) influxdb (~> 0.7.0) irb irix (~> 2.6.0) - jwt (~> 2.2.0) + jwt! jwt-multisig (~> 1.0.0) jwt-rack (~> 0.1.0) kaminari (~> 1.2.1) @@ -521,12 +572,14 @@ DEPENDENCIES ransack (~> 2.3.2) rbtree (~> 0.4.2) redis (~> 4.1.2) + rspec rspec-rails (~> 3.8, >= 3.8.2) rspec-retry (~> 0.6) rubocop-rspec (~> 1.32) ruby-prof (~> 0.17.0) safe_yaml (~> 1.0.5) scout_apm (~> 2.4) + semver2 (~> 3.4) sentry-raven (~> 2.9.0) timecop (~> 0.9) validate_url (~> 1.0.4) @@ -539,4 +592,4 @@ RUBY VERSION ruby 2.6.6p146 BUNDLED WITH - 2.1.4 + 2.2.15 diff --git a/Gemfile.plugin b/Gemfile.plugin index a1a43e7466..aedc6f72bc 100644 --- a/Gemfile.plugin +++ b/Gemfile.plugin @@ -9,3 +9,4 @@ gem 'peatio-bitcoincash', '~> 2.6.1' gem 'peatio-ripple', '~> 2.6.1' gem 'peatio-bitgo', '~> 2.6.6' gem 'peatio-electrum', '~> 2.6.2' +gem 'bitzlato', github: 'finfex/bitzlato', branch: 'main' diff --git a/Guardfile b/Guardfile new file mode 100644 index 0000000000..f8404d9003 --- /dev/null +++ b/Guardfile @@ -0,0 +1,75 @@ +# A sample Guardfile +# More info at https://github.com/guard/guard#readme + +## Uncomment and set this to only include directories you want to watch +# directories %w(app lib config test spec features) \ +# .select{|d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist")} + +## Note: if you are using the `directories` clause above and you are not +## watching the project directory ('.'), then you will want to move +## the Guardfile to a watched dir and symlink it back, e.g. +# +# $ mkdir config +# $ mv Guardfile config/ +# $ ln -s config/Guardfile . +# +# and, you'll have to watch "config/Guardfile" instead of "Guardfile" +# +notification :tmux, display_message: true if ENV.has_key?('TMUX') + +# Note: The cmd option is now required due to the increasing number of ways +# rspec may be run, below are examples of the most common uses. +# * bundler: 'bundle exec rspec' +# * bundler binstubs: 'bin/rspec' +# * spring: 'bin/rspec' (This will use spring if running and you have +# installed the spring binstubs per the docs) +# * zeus: 'zeus rspec' (requires the server to be started separately) +# * 'just' rspec: 'rspec' + +guard :rspec, cmd: "bundle exec rspec" do + require "guard/rspec/dsl" + dsl = Guard::RSpec::Dsl.new(self) + + # Feel free to open issues for suggestions and improvements + + # RSpec files + rspec = dsl.rspec + watch(rspec.spec_helper) { rspec.spec_dir } + watch(rspec.spec_support) { rspec.spec_dir } + watch(rspec.spec_files) + + # Ruby files + ruby = dsl.ruby + dsl.watch_spec_files_for(ruby.lib_files) + + # Rails files + rails = dsl.rails(view_extensions: %w(erb haml slim)) + dsl.watch_spec_files_for(rails.app_files) + dsl.watch_spec_files_for(rails.views) + + watch(rails.controllers) do |m| + [ + rspec.spec.call("routing/#{m[1]}_routing"), + rspec.spec.call("controllers/#{m[1]}_controller"), + rspec.spec.call("acceptance/#{m[1]}") + ] + end + + # Rails config changes + watch(rails.spec_helper) { rspec.spec_dir } + watch(rails.routes) { "#{rspec.spec_dir}/routing" } + watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" } + + # Capybara features specs + watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") } + watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") } + + # Turnip features and steps + watch(%r{^spec/acceptance/(.+)\.feature$}) + watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m| + Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance" + end + + # Rake tasks + dsl.watch_spec_files_for(dsl.rake.rake_files) +end diff --git a/README.md b/README.md index 5f146a8571..53dabefab7 100644 --- a/README.md +++ b/README.md @@ -228,4 +228,3 @@ Peatio is released under the terms of the [MIT license](http://peatio.mit-licens [Peatio](http://en.wikipedia.org/wiki/Pixiu) (Chinese: 貔貅) is a Chinese mythical hybrid creature considered to be a very powerful protector to practitioners of Feng Shui. - diff --git a/app/api/v2/account/deposits.rb b/app/api/v2/account/deposits.rb index ecaf60a26b..51b7854c92 100644 --- a/app/api/v2/account/deposits.rb +++ b/app/api/v2/account/deposits.rb @@ -7,9 +7,40 @@ module API module V2 module Account class Deposits < Grape::API - before { deposits_must_be_permitted! } + desc 'Create deposit intention', + success: API::V2::Entities::Deposit + params do + requires :currency, + type: String, + values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist' }, + desc: 'Currency code' + requires :amount, + type: BigDecimal, + desc: 'The deposit amount.' + end + + post '/deposits/intention' do + currency = Currency.find(params[:currency]) + + unless currency.deposit_enabled? + error!({ errors: ['management.currency.deposit_disabled'] }, 422) + end + + wallet = Wallet.deposit_wallet(currency.id) + + unless wallet.present? + error!({ errors: ['account.wallet.not_found'] }, 422) + end + + deposit = WalletService + .new(wallet) + .create_deposit_intention!(current_user, currency, params[:amount]) + + present deposit, with: API::V2::Entities::Deposit + end + desc 'Get your deposits history.', is_array: true, success: API::V2::Entities::Deposit diff --git a/app/api/v2/entities/account.rb b/app/api/v2/entities/account.rb index 83e63def4a..edb2ce8a4c 100644 --- a/app/api/v2/entities/account.rb +++ b/app/api/v2/entities/account.rb @@ -34,7 +34,7 @@ class Account < Base expose( :deposit_address, - if: ->(account, _options) { account.currency.coin? }, + if: ->(account, _options) { account.currency.coin? && !Wallet.deposit_wallet(account.currency_id)&.settings&.fetch('enable_intention', false) }, using: API::V2::Entities::PaymentAddress, documentation: { desc: 'User deposit address', @@ -44,6 +44,17 @@ class Account < Base wallet = Wallet.deposit_wallet(account.currency_id) ::PaymentAddress.find_by(wallet: wallet, member: options[:current_user], remote: false) end + + expose( + :enable_intention, + if: ->(account, _options) { Wallet.deposit_wallet(account.currency_id)&.settings&.fetch('enable_intention', false) }, + documentation: { + desc: 'Show intention form instead of payment address generation', + type: JSON + } + ) do |account, options| + true + end end end end diff --git a/app/api/v2/entities/deposit.rb b/app/api/v2/entities/deposit.rb index 034d47f9c8..3f48e86af0 100644 --- a/app/api/v2/entities/deposit.rb +++ b/app/api/v2/entities/deposit.rb @@ -73,6 +73,20 @@ class Deposit < Base } ) + expose( + :transfer_links, + documentation: { + type: JSON, + desc: 'Links to p2p page to make deposit transfer', + example: -> { + [ + { title: 'telegram', url: 'https://t.me/BTC_STAGE_BOT?start=b_0f8c3db61f223ea9df072fd37e0b6315' }, + { title: 'web', url: 'https://s-www.lgk.one/p2p/?start=b_0f8c3db61f223ea9df072fd37e0b6315' } + ] + } + }, + if: ->(deposit) { deposit.transfer_links.present? } + ) expose( :created_at, diff --git a/app/api/v2/entities/withdraw.rb b/app/api/v2/entities/withdraw.rb index 943eb2fb59..c7e016327f 100644 --- a/app/api/v2/entities/withdraw.rb +++ b/app/api/v2/entities/withdraw.rb @@ -117,6 +117,23 @@ class Withdraw < Base desc: 'The datetime when withdraw was completed' } ) + + expose( + :transfer_links, + if: ->(withdraw) { withdraw.metadata.has_key? 'links' }, + documentation: { + type: JSON, + desc: 'Links to confirm withdraw on external resource', + example: -> { + [ + { title: 'telegram', url: 'https://t.me/BTC_STAGE_BOT?start=b_0f8c3db61f223ea9df072fd37e0b6315' }, + { title: 'web', url: 'https://s-www.lgk.one/p2p/?start=b_0f8c3db61f223ea9df072fd37e0b6315' } + ] + } + } + ) do |withdraw| + withdraw.metadata['links'] + end end end end diff --git a/app/api/v2/locale_helpers.rb b/app/api/v2/locale_helpers.rb new file mode 100644 index 0000000000..e995062288 --- /dev/null +++ b/app/api/v2/locale_helpers.rb @@ -0,0 +1,18 @@ +module API + module V2 + module LocaleHelpers + def available_locales + @available_locales ||= I18n.available_locales.map(&:to_s) + end + + def available_locale(locale) + locale if available_locales.include? locale + end + + def request_locale + available_locale(params[:locale]) || + request.env.http_accept_language.preferred_language_from(I18n.available_locales) + end + end + end +end diff --git a/app/api/v2/mount.rb b/app/api/v2/mount.rb index 8be80ba188..764d024523 100644 --- a/app/api/v2/mount.rb +++ b/app/api/v2/mount.rb @@ -13,6 +13,11 @@ class Mount < Grape::API default_format :json helpers V2::Helpers + helpers V2::LocaleHelpers + + before do + I18n.locale = request_locale || I18n.locale + end do_not_route_options! diff --git a/app/jobs/cron/transfers_polling.rb b/app/jobs/cron/transfers_polling.rb new file mode 100644 index 0000000000..a1d7e342c4 --- /dev/null +++ b/app/jobs/cron/transfers_polling.rb @@ -0,0 +1,29 @@ +# Run polling process for supported gateways +module Jobs + module Cron + module TransfersPolling + def self.process + Wallet.active.find_each do |w| + ws = WalletService .new(w) + poll_deposits ws if ws.support_deposits_polling? + poll_withdraws ws if ws.support_withdraws_polling? + end + sleep 10 + rescue StandardError => e + report_exception(e) + end + + def self.poll_withdraws(ws) + ws.poll_withdraws! + rescue StandardError => e + report_exception(e) + end + + def self.poll_deposits(ws) + ws.poll_deposits! + rescue StandardError => e + report_exception(e) + end + end + end +end diff --git a/app/jobs/cron/wallet_balances.rb b/app/jobs/cron/wallet_balances.rb index 8769e1fa66..5153038f80 100644 --- a/app/jobs/cron/wallet_balances.rb +++ b/app/jobs/cron/wallet_balances.rb @@ -5,7 +5,7 @@ def self.process Wallet.active.find_each do |w| w.update!(balance: w.current_balance) rescue StandardError => e - report_exception_to_screen(e) + report_exception(e) next end sleep 60 diff --git a/app/models/app_version.rb b/app/models/app_version.rb new file mode 100644 index 0000000000..f56f7dd011 --- /dev/null +++ b/app/models/app_version.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'semver' +AppVersion = SemVer.find + +# Deployed version has ./REVISION file in root directory +# +revision = Rails.root.join('REVISION') +AppVersion.metadata = File.read(revision).chomp if File.exist? revision diff --git a/app/models/deposit.rb b/app/models/deposit.rb index 4aa3fb77ee..ec3d2a5f76 100644 --- a/app/models/deposit.rb +++ b/app/models/deposit.rb @@ -6,6 +6,7 @@ class Deposit < ApplicationRecord serialize :spread, Array serialize :from_addresses, Array + serialize :data, JSON unless Rails.configuration.database_support_json include AASM include AASM::Locking @@ -122,6 +123,13 @@ def aml_check! true end + def transfer_links + # TODO rename data['links'] to transfer_links + # TODO rename data['expires_at'] to expires_at + # TODO Use txid instead of intention_id + data&.fetch 'links', [] + end + def blockchain_api currency.blockchain_api end diff --git a/app/models/order.rb b/app/models/order.rb index 84dba7b7ff..9968ab8c2d 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -180,7 +180,7 @@ def submit_order compute_locked end - raise ::Account::AccountError unless member_balance >= locked + raise ::Account::AccountError, "member_balance > locked = #{member_balance}>#{locked}" unless member_balance >= locked return trigger_third_party_creation unless market.engine.peatio_engine? diff --git a/app/models/wallet.rb b/app/models/wallet.rb index f0526f2375..4e03fce26c 100644 --- a/app/models/wallet.rb +++ b/app/models/wallet.rb @@ -69,7 +69,7 @@ class Wallet < ApplicationRecord end before_validation do - next unless address? && blockchain.blockchain_api.supports_cash_addr_format? + next unless address? && blockchain.try(:blockchain_api).try(:supports_cash_addr_format?) self.address = CashAddr::Converter.to_cash_address(address) end diff --git a/app/services/wallet_service.rb b/app/services/wallet_service.rb index d1513e118a..b829f7be10 100644 --- a/app/services/wallet_service.rb +++ b/app/services/wallet_service.rb @@ -6,6 +6,98 @@ def initialize(wallet) @adapter = Peatio::Wallet.registry[wallet.gateway.to_sym].new(wallet.settings.symbolize_keys) end + def create_deposit_intention!(member, currency, amount) + @adapter.configure(wallet: @wallet.to_wallet_api_settings, + currency: { id: currency.id }) + + intention = @adapter.create_deposit_intention!( + amount: amount, + comment: I18n.t('deposit_comment', account_id: member.uid) + ) + Deposit.create!( + type: Deposit.name, + member: member, + data: intention.slice(:links, :expires_at), + currency: currency, + amount: intention[:amount], + intention_id: intention[:id] + ) + end + + def support_deposits_polling? + @adapter.respond_to?(:poll_deposits) && @wallet.settings['allow_deposits_polling'] + end + + def support_withdraws_polling? + @adapter.respond_to?(:poll_withdraws) && @wallet.settings['allow_withdraws_polling'] + end + + def poll_withdraws! + @wallet.currencies.each do |currency| + @adapter.configure(wallet: @wallet.to_wallet_api_settings, + currency: { id: currency.id }) + + @adapter.poll_withdraws.each do |withdraw_info| + next unless withdraw_info.is_done + next if withdraw_info.withdraw_id.nil? + withdraw = Withdraw.find_by(id: withdraw_info.withdraw_id) + if withdraw.nil? + Rails.logger.warn("No such withdraw withdraw_info ##{withdraw_info.id} for #{currency.id} in wallet #{@wallet.name}") + next + end + if withdraw.amount!=withdraw_info.amount + Rails.logger.error("Withdraw and intention amounts are not equeal #{withdraw.amount}<>#{withdraw_info.amount} with withdraw_info ##{withdraw_info.id} for #{currency.id} in wallet #{@wallet.name}") + next + end + unless withdraw.confirming? + Rails.logger.warn("Withdraw #{withdraw.id} has wrong status (#{withdraw.aasm_state})") + next + end + + Rails.logger.info("Withdraw #{withdraw.id} successed") + withdraw.success! + end + end + end + + def poll_deposits! + @wallet.currencies.each do |currency| + @adapter.configure(wallet: @wallet.to_wallet_api_settings, + currency: { id: currency.id }) + + @adapter.poll_deposits.each do |intention| + deposit = Deposit.find_by(currency: currency, intention_id: intention[:id]) + if deposit.present? + if deposit.amount==intention[:amount] + deposit.with_lock do + if deposit.submitted? + deposit.accept! + + # Save beneficiary for future withdraws + if @wallet.settings['save_beneficiary'] + if intention[:address].present? + Rails.logger.info("Save #{intention[:address]} as beneficiary for #{deposit.account.id}") + deposit.account.member.beneficiaries + .create_with(data: { address: intention[:address] }, state: :active) + .find_or_create_by!(name: [@wallet.settings['beneficiary_prefix'], intention[:address]].compact.join(':'), currency: currency) + else + Rails.logger.warn("Deposit #{deposit.id} has no address to save to beneficiaries") + end + end + else + Rails.logger.warn("Deposit #{deposit.id} has wrong status (#{deposit.aasm_state})") unless deposit.accepted? + end + end + else + Rails.logger.warn("Deposit and intention amounts are not equeal #{deposit.amount}<>#{intention[:amount]} with intention ##{intention[:id]} for #{currency.id} in wallet #{@wallet.name}") + end + else + Rails.logger.warn("No such deposit intention ##{intention[:id]} for #{currency.id} in wallet #{@wallet.name}") + end + end + end + end + def create_address!(uid, pa_details) @adapter.configure(wallet: @wallet.to_wallet_api_settings, currency: @wallet.currencies.first.to_blockchain_api_settings) @@ -18,9 +110,16 @@ def build_withdrawal!(withdrawal) transaction = Peatio::Transaction.new(to_address: withdrawal.rid, amount: withdrawal.amount, currency_id: withdrawal.currency_id, - options: { tid: withdrawal.tid }) + options: { tid: withdrawal.tid, withdrawal_id: withdrawal.id }) + transaction = @adapter.create_transaction!(transaction) - save_transaction(transaction.as_json.merge(from_address: @wallet.address), withdrawal) if transaction.present? + + withdrawal.with_lock do + save_transaction(transaction.as_json.merge(from_address: @wallet.address), withdrawal) + withdrawal.update metadata: withdrawal.metadata.merge( 'links' => transaction.options['links'] ) if transaction.options&.has_key? 'links' + withdrawal.success! if withdrawal.confirming? && transaction.status == 'succeed' + end if transaction.present? + transaction end diff --git a/app/workers/daemons/cron_job.rb b/app/workers/daemons/cron_job.rb index 67927477ad..126b953402 100644 --- a/app/workers/daemons/cron_job.rb +++ b/app/workers/daemons/cron_job.rb @@ -6,7 +6,15 @@ module Workers module Daemons class CronJob < Base - JOBS = [Jobs::Cron::KLine, Jobs::Cron::Ticker, Jobs::Cron::StatsMemberPnl, Jobs::Cron::AML, Jobs::Cron::Refund, Jobs::Cron::WalletBalances].freeze + JOBS = [ + Jobs::Cron::KLine, + Jobs::Cron::Ticker, + Jobs::Cron::StatsMemberPnl, + Jobs::Cron::AML, + Jobs::Cron::Refund, + Jobs::Cron::WalletBalances, + Jobs::Cron::TransfersPolling, + ].freeze def run JOBS.map { |j| Thread.new { process(j) } }.map(&:join) diff --git a/bin/_guard-core b/bin/_guard-core new file mode 100755 index 0000000000..cd565c3a6c --- /dev/null +++ b/bin/_guard-core @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application '_guard-core' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +bundle_binstub = File.expand_path("../bundle", __FILE__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("guard", "_guard-core") diff --git a/bin/guard b/bin/guard new file mode 100755 index 0000000000..bcb966f4c1 --- /dev/null +++ b/bin/guard @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'guard' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +bundle_binstub = File.expand_path("../bundle", __FILE__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("guard", "guard") diff --git a/config/application.rb b/config/application.rb index 383ae6612e..82a54f60fd 100644 --- a/config/application.rb +++ b/config/application.rb @@ -37,7 +37,7 @@ class Application < Rails::Application # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. - config.time_zone = ENV.fetch('TIMEZONE', 'UTC') + config.time_zone = ENV.fetch('TIMEZONE', File.read('/etc/timezone').strip) # Configure relative url root by setting URL_ROOT_PATH environment variable. # Used by microkube with API Gateway. diff --git a/config/initializers/i18n.rb b/config/initializers/i18n.rb new file mode 100644 index 0000000000..926a46d196 --- /dev/null +++ b/config/initializers/i18n.rb @@ -0,0 +1 @@ +Rails.application.config.i18n.available_locales = ENV.fetch('AVAILABLE_LOCALES', 'en ru').split diff --git a/config/initializers/versioning.rb b/config/initializers/versioning.rb index bf40a71401..5054c16303 100644 --- a/config/initializers/versioning.rb +++ b/config/initializers/versioning.rb @@ -8,6 +8,6 @@ class Application GIT_TAG = '3.1.0' GIT_SHA = '36895e2' BUILD_DATE = '2021-07-04 11:43:58+00:00' - VERSION = GIT_TAG + VERSION = AppVersion.to_s end end diff --git a/config/initializers/wallet_api.rb b/config/initializers/wallet_api.rb index fc1287ef8f..19297c3402 100644 --- a/config/initializers/wallet_api.rb +++ b/config/initializers/wallet_api.rb @@ -1,3 +1,6 @@ +require 'peatio/owhdwallet/wallet' +require 'peatio/opendax_cloud/wallet' +require 'peatio/bitzlato/wallet' Peatio::Wallet.registry[:bitcoind] = Bitcoin::Wallet Peatio::Wallet.registry[:geth] = Ethereum::Wallet Peatio::Wallet.registry[:parity] = Ethereum::Wallet @@ -5,3 +8,4 @@ Peatio::Wallet.registry[:ow_hdwallet] = OWHDWallet::Wallet Peatio::Wallet.registry[:opendax] = OWHDWallet::Wallet Peatio::Wallet.registry[:opendax_cloud] = OpendaxCloud::Wallet +Peatio::Wallet.registry[:bitzlato] = Bitzlato::Wallet diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000000..74d139b92b --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,2 @@ +en: + deposit_comment: Excange service deposit for account %{account_id} diff --git a/config/locales/ru.yml b/config/locales/ru.yml new file mode 100644 index 0000000000..a4a5f3d5d8 --- /dev/null +++ b/config/locales/ru.yml @@ -0,0 +1,2 @@ +ru: + deposit_comment: Пополнение биржевого счёта %{account_id} diff --git a/db/migrate/20210311145918_add_data_to_deposits.rb b/db/migrate/20210311145918_add_data_to_deposits.rb new file mode 100644 index 0000000000..da4922e60a --- /dev/null +++ b/db/migrate/20210311145918_add_data_to_deposits.rb @@ -0,0 +1,6 @@ +class AddDataToDeposits < ActiveRecord::Migration[5.2] + def change + add_column :deposits, :data, :json + add_column :deposits, :intention_id, :bigint + end +end diff --git a/db/migrate/20210317141836_add_unique_index_to_intention_id_in_deposits.rb b/db/migrate/20210317141836_add_unique_index_to_intention_id_in_deposits.rb new file mode 100644 index 0000000000..8282241706 --- /dev/null +++ b/db/migrate/20210317141836_add_unique_index_to_intention_id_in_deposits.rb @@ -0,0 +1,5 @@ +class AddUniqueIndexToIntentionIdInDeposits < ActiveRecord::Migration[5.2] + def change + add_index :deposits, [:currency_id, :intention_id], unique: true, where: 'intention_id is not null' + end +end diff --git a/db/migrate/20210416125059_change_deposit_intention_type_to_string.rb b/db/migrate/20210416125059_change_deposit_intention_type_to_string.rb new file mode 100644 index 0000000000..fd34877bf7 --- /dev/null +++ b/db/migrate/20210416125059_change_deposit_intention_type_to_string.rb @@ -0,0 +1,5 @@ +class ChangeDepositIntentionTypeToString < ActiveRecord::Migration[5.2] + def change + change_column :deposits, :intention_id, :string + end +end diff --git a/lib/daemons/daemons.god b/lib/daemons/daemons.god index 66756db718..3ff342b21b 100644 --- a/lib/daemons/daemons.god +++ b/lib/daemons/daemons.god @@ -96,9 +96,9 @@ daemon 'daemon:blockchain', script: 'daemons.rb', arguments: %w[ blockchain ] -daemon 'daemon:k', +daemon 'daemon:cron_job', script: 'daemons.rb', - arguments: %w[ k ] + arguments: %w[ cron_job ] daemon 'daemon:global_state', script: 'daemons.rb', diff --git a/lib/peatio/bitzlato/wallet.rb b/lib/peatio/bitzlato/wallet.rb new file mode 100644 index 0000000000..539e6575b0 --- /dev/null +++ b/lib/peatio/bitzlato/wallet.rb @@ -0,0 +1,179 @@ +require 'digest' + +module Bitzlato + class Wallet < Peatio::Wallet::Abstract + class WithdrawInfo + attr_accessor :is_done, :withdraw_id, :currency, :amount + end + + WITHDRAW_METHODS = %w[voucher payment] + WITHDRAW_POLLING_METHODS = %w[vouchers payments] + + def initialize(features = {}) + @features = features + @settings = {} + end + + def configure(settings = {}) + # Clean client state during configure. + @client = nil + + @settings = settings + + @wallet = @settings.fetch(:wallet) do + raise Peatio::Wallet::MissingSettingError, :wallet + end.slice(:uri, :key, :uid) + + @currency = @settings.fetch(:currency) do + raise Peatio::Wallet::MissingSettingError, :currency + end.slice(:id) + end + + def create_transaction!(transaction, options = {}) + withdraw_method = @settings.dig(:wallet, :withdraw_method) + raise Peatio::Wallet::MissingSettingError, 'wallet.withdraw_method' unless WITHDRAW_METHODS.include? withdraw_method + + case withdraw_method + when 'voucher' + create_voucher! transaction, options + when 'payment' + create_payment! transaction, options + else + raise Peatio::Wallet::ClientError, "Unknown withdraw_polling_method specified (#{@withdraw_method})" + end + end + + def create_payment!(transaction, options = {}) + key = transaction.options[:withdrawal_id] || raise("No withdrawal ID") + response = client.post( + '/api/gate/v1/payments/create', + { clientProvidedId: key, client: transaction.to_address, cryptocurrency: transaction.currency_id.upcase, amount: transaction.amount, payedBefore: true } + ) + payment_id = response['paymentId'] || raise("No payment ID in response") + transaction.hash = transaction.txout = generate_id payment_id + transaction.options.merge! payment_id: payment_id + transaction.status = 'succeed' + transaction + rescue Bitzlato::Client::Error => e + raise Peatio::Wallet::ClientError, e + end + + def create_voucher!(transaction, options = {}) + voucher = client.post( + '/api/p2p/vouchers/', + { cryptocurrency: transaction.currency_id.upcase, amount: transaction.amount, method: 'crypto', currency: 'USD'} + ) + + transaction.options.merge!( + 'voucher' => voucher, + 'links' => voucher['links'].map { |link| { 'title' => link['type'], 'url' => link['url'] } } + ) + + transaction.txout = voucher['deepLinkCode'] + transaction.hash = voucher['deepLinkCode'] + transaction + rescue Bitzlato::Client::Error => e + raise Peatio::Wallet::ClientError, e + end + + def load_balance! + # TODO fetch actual balance + 999_999_999 # Yeah! + end + + def create_deposit_intention!(amount: , comment:) + response = client + .post('/api/gate/v1/invoices/', { + cryptocurrency: currency_id.to_s.upcase, + amount: amount, + comment: comment + }) + + { + amount: response['amount'].to_d, + username: response['username'], + id: generate_id(response['id']), + links: response['link'].each_with_object([]) { |e, a| a << { 'title' => e.first, 'url' => e.second } }, + expires_at: Time.at(response['expiryAt']/1000) + } + end + + def poll_deposits + client + .get('/api/gate/v1/invoices/transactions/')['data'] + .map do |transaction| + { + address: transaction['username'], + id: generate_id(transaction['invoiceId']), + amount: transaction['amount'].to_d, + currency: transaction['cryptocurrency'] + } + end + end + + def poll_withdraws + withdraws = [] + + withdraw_polling_methods = @settings.dig(:wallet, :withdraw_polling_methods) + raise Peatio::Wallet::MissingSettingError, 'wallet.withdraw_polling_methods' unless withdraw_polling_methods.present? && (withdraw_polling_methods - WITHDRAW_POLLING_METHODS).empty? + + withdraw_polling_methods.each do |method| + case method + when 'vouchers' + withdraws += poll_vouchers + when 'payments' + withdraws += poll_payments + else + Rails.logger.error("Unknown withdraw_polling_methods (#{method})") + next + end + end + + withdraws + end + + private + + def poll_payments + client + .get('/api/gate/v1/payments/list/') + .map do |payment| + WithdrawInfo.new.tap do |w| + w.withdraw_id = payment['clientProvidedId'] + w.is_done = payment['status'] == 'done' + w.amount = payment['amount'].to_d + w.currency = payment['cryptocurrency'] + end + end + end + + def poll_vouchers + client + .get('/api/p2p/vouchers/')['data'] + .map do |voucher| + WithdrawInfo.new.tap do |w| + w.withdraw_id = voucher['deepLinkCode'] + w.is_done = voucher['status'] == 'cashed' + w.amount = voucher.dig('cryptocurrency', 'amount').to_d + w.currency = voucher.dig('cryptocurrency', 'code').downcase + end + end + end + + def currency_id + @currency.fetch(:id) + end + + def generate_id id + [client.uid, id] * ':' + end + + def client + @client ||= Bitzlato::Client + .new(home_url: ENV.fetch('BITZLATO_API_URL', @wallet.fetch(:uri)), + key: ENV.fetch('BITZLATO_API_KEY', @wallet.fetch(:key)).yield_self { |key| key.is_a?(String) ? JSON.parse(key) : key }.transform_keys(&:to_sym), + uid: ENV.fetch('BITZLATO_API_CLIENT_UID', @wallet.fetch(:uid)).to_i, + logger: ENV.true?('BITZLATO_API_LOGGER')) + end + end +end diff --git a/lib/tasks/seed.rake b/lib/tasks/seed.rake index 194b7c1904..88c2c7405c 100644 --- a/lib/tasks/seed.rake +++ b/lib/tasks/seed.rake @@ -18,8 +18,12 @@ namespace :seed do task currencies: :environment do Currency.transaction do YAML.load_file(Rails.root.join('config/seed/currencies.yml')).each do |hash| - next if Currency.exists?(id: hash.fetch('id')) - Currency.create!(hash) + currency = Currency.find_by(id: hash.fetch('id')) + if currency.present? + currency.update! hash + else + Currency.create!(hash) + end end end end @@ -50,7 +54,7 @@ namespace :seed do YAML.load_file(Rails.root.join('config/seed/markets.yml')) .map(&:symbolize_keys) .each do |hash| - next if Market.exists?(id: hash.fetch(:id)) + next if Market.exists?(symbol: hash.fetch(:id)) # For compatibility with old markets.yml # If state is not defined set it from enabled. enabled = hash.delete(:enabled) @@ -65,7 +69,8 @@ namespace :seed do min_ask_price: :min_price, max_bid_price: :max_price, min_ask_amount: :min_amount, - min_bid_amount: :min_amount } + min_bid_amount: :min_amount, + symbol: :id } legacy_keys_mappings.each do |old_key, new_key| legacy_key_value = hash.delete(old_key) @@ -89,11 +94,15 @@ namespace :seed do task wallets: :environment do Wallet.transaction do YAML.load_file(Rails.root.join('config/seed/wallets.yml')).each do |hash| - next if Wallet.exists?(name: hash.fetch('name')) if hash['currency_ids'].is_a?(String) hash['currency_ids'] = hash['currency_ids'].split(',') end - Wallet.create!(hash) + wallet = Wallet.find_by(name: hash.fetch('name')) + if wallet.present? + wallet.update! hash + else + Wallet.create!(hash) + end end end end diff --git a/spec/api/v2/account/deposits_spec.rb b/spec/api/v2/account/deposits_spec.rb index 4d98dddb8b..d8f3ea57a1 100644 --- a/spec/api/v2/account/deposits_spec.rb +++ b/spec/api/v2/account/deposits_spec.rb @@ -12,6 +12,26 @@ Ability.stubs(:user_permissions).returns({'member'=>{'read'=>['Deposit', 'PaymentAddress']}}) end + describe 'POST /api/v2/account/deposits/intention' do + let(:wallet) { Wallet.deposit.joins(:currencies).find_by(currencies: { id: currency }) } + let(:amount) { 12.1231 } + it 'requires authentication' do + api_post '/api/v2/account/deposits/intention', params: { amount: 123 } + expect(response.code).to eq '401' + end + + it 'returns with auth token deposits' do + Wallet.any_instance.stubs(:gateway).returns(:bitzlato) + Wallet.any_instance.stubs(:settings).returns({ key: {}, uid: :some_acount_id, uri: 'uri' }) + Bitzlato::Wallet.any_instance.stubs(:create_deposit_intention!).returns({ amount: amount, id: :unique_intention_id, links: {}}) + api_post '/api/v2/account/deposits/intention', token: token, params: { currency: :btc, amount: amount } + + expect(response).to be_successful + result = JSON.parse(response.body) + expect(result['amount']).to eq amount.to_s + end + end + describe 'GET /api/v2/account/deposits' do before do create(:deposit_btc, member: member, updated_at: 5.days.ago) diff --git a/spec/lib/peatio/bitzlato/wallet_spec.rb b/spec/lib/peatio/bitzlato/wallet_spec.rb new file mode 100644 index 0000000000..7a235e80ad --- /dev/null +++ b/spec/lib/peatio/bitzlato/wallet_spec.rb @@ -0,0 +1,260 @@ +describe Bitzlato::Wallet do + let(:wallet) { Bitzlato::Wallet.new } + + describe :requests do + around do |example| + WebMock.disable_net_connect! + example.run + WebMock.allow_net_connect! + end + + before do + wallet.configure(settings) + ENV['BITZLATO_API_KEY']=nil + ENV['BITZLATO_API_URL']=nil + ENV['BITZLATO_API_CLIENT_UID']=nil + end + + let(:uri) { 'http://127.0.0.1:8000' } + let(:key) { + {"kty":"EC","alg":"ES256","crv":"P-256", + "x":"wwf6h_sZhv6TXAYz4XrdXZVpLo_uoNESbaEf_zEydus", + "y":"OL-0AqcTNoaCBVAEpDNsU1bpZA7eQ9CtGPZGmEEg5QI", + "d":"nDTvKjSPQ4UAPiBmJKXeF1MKhuhLtjJtW6hypstWolk"} + } + + let(:withdraw_method) { 'voucher' } + + let(:settings) do + { + wallet: { uri: uri, key: key, uid: 'merchant_uid', withdraw_method: withdraw_method, withdraw_polling_methods: ['vouchers', 'payments'] }, + currency: { id: :btc }, + } + end + + context :poll_payments do + let(:response) do + [ + { + "publicName": "dapi", + "links": nil, + "amount": 0.21, + "cryptocurrency": "BTC", + "type": "auto", + "status": "done", + "date": 1616396531426 + }, + { + "publicName": "dapi", + "links": nil, + "amount": 0.2, + "cryptocurrency": "BTC", + "type": "auto", + "status": "done", + "date": 1616396505639 + }, + { + "publicName": "dapi", + "links": nil, + "amount": 0.19, + "cryptocurrency": "BTC", + "type": "auto", + "status": "done", + "date": 1616396365270 + } + ] + end + + it do + stub_request(:get, "http://127.0.0.1:8000/api/gate/v1/payments/list/") + .to_return(body: response.to_json, headers: { 'Content-Type': 'application/json' }) + payments = wallet.send :poll_payments + expect(payments.count).to eq 3 + expect(payments.second.is_done).to be_truthy + end + end + + context :poll_vouchers do + let(:response) do + { + "total": 2, + "data": [ + { + "deepLinkCode": "c_c8e8f34d2fff9f5dbc222939feeefbe5", + "currency": { + "code": "USD", + "amount": "5385" + }, + "cryptocurrency": { + "code": "BTC", + "amount": "1" + }, + "createdAt": 1616127762606, + "links": [ + { + "type": "telegram bot @BTC_STAGE_BOT", + "url": "https://telegram.me/BTC_STAGE_BOT?start=c_c8e8f34d2fff9f5dbc222939feeefbe5" + }, + { + "type": "web exchange", + "url": "https://s-www.lgk.one/p2p/?start=c_c8e8f34d2fff9f5dbc222939feeefbe5" + } + ], + "status": "none", + "cashedBy": "EasySammieFrey" + }, + { + "deepLinkCode": "c_c8e8f34d2fff9f5dbc222939feeefbe5", + "currency": { + "code": "USD", + "amount": "5385" + }, + "cryptocurrency": { + "code": "BTC", + "amount": "0.0931216" + }, + "createdAt": 1616127762606, + "links": [ + { + "type": "telegram bot @BTC_STAGE_BOT", + "url": "https://telegram.me/BTC_STAGE_BOT?start=c_c8e8f34d2fff9f5dbc222939feeefbe5" + }, + { + "type": "web exchange", + "url": "https://s-www.lgk.one/p2p/?start=c_c8e8f34d2fff9f5dbc222939feeefbe5" + } + ], + "status": "cashed", + "cashedBy": "EasySammieFrey" + }, + ] } + end + + it do + stub_request(:get, "http://127.0.0.1:8000/api/p2p/vouchers/") + .to_return(body: response.to_json, headers: { 'Content-Type': 'application/json' }) + vouchers = wallet.send :poll_vouchers + expect(vouchers.count).to eq 2 + expect(vouchers.first.is_done).to be_falsey + expect(vouchers.second.is_done).to be_truthy + end + end + + context :poll_withdraws do + it do + wallet.expects(:poll_vouchers).returns [] + wallet.expects(:poll_payments).returns [] + withdraws = wallet.poll_withdraws + + expect(withdraws).to be_a Array + end + end + + + context :create_transaction! do + let(:source_transaction) { + Peatio::Transaction.new(to_address: 1, + amount: 123, + currency_id: 'BTC', + options: { withdrawal_id: 12, tid: 'tid' }) + } + + context :voucher do + let(:withdraw_method) { 'voucher' } + it 'create withdrawal transaction' do + wallet.expects(:create_voucher!) + wallet.create_transaction!(source_transaction) + end + end + + context :payment do + let(:withdraw_method) { 'payment' } + it 'create withdrawal transaction' do + wallet.expects(:create_payment!) + wallet.create_transaction!(source_transaction) + end + end + + context :create_payment! do + let(:response) { { paymentId: 12 }} + it 'show create withdrawal transaction' do + stub_request(:post, uri + '/api/gate/v1/payments/create') + .with( body: "{\"clientProvidedId\":12,\"client\":1,\"cryptocurrency\":\"BTC\",\"amount\":123,\"payedBefore\":true}" ) + .to_return(body: response.to_json, headers: { 'Content-Type': 'application/json' }) + + transaction = wallet.create_payment!(source_transaction) + + expect(transaction).to be_a Peatio::Transaction + expect(transaction.txout).to be_present + expect(transaction.hash).to be_present + end + end + + context :create_voucher! do + let(:response) do + { + "deepLinkCode"=>"someHash", + "currency"=>{"code"=>"USD", "amount"=>"6965"}, + "cryptocurrency"=>{"code"=>"BTC", "amount"=>"0.12"}, + "createdAt"=>1616075809783, + "links"=>[ + {"type"=>"telegram bot @BTC_STAGE_BOT", "url"=>"https://telegram.me/BTC_STAGE_BOT?start=someHash"}, + {"type"=>"web exchange", "url"=>"https://s-www.lgk.one/p2p/?start=someHash"} + ], + "status"=>"active", + "cashedBy"=>nil, + "comment"=>nil + } + end + + it 'create voucher' do + stub_request(:post, uri + '/api/p2p/vouchers/') + .with(body: {"cryptocurrency":"BTC","amount":123,"method":"crypto", 'currency': 'USD'}.to_json) + .to_return(body: response.to_json, headers: { 'Content-Type': 'application/json' }) + + + transaction = wallet.create_voucher!(source_transaction) + + expect(transaction).to be_a Peatio::Transaction + expect(transaction.txout).to be_present + expect(transaction.hash).to be_present + expect(transaction.options['voucher']).to be_present + expect(transaction.options['links']).to be_a(Array) + expect(transaction.options['links'].count).to eq(2) + expect(transaction.options['links'].first.keys).to eq(['title', 'url']) + end + end + end + + context :create_deposit_intention! do + let(:response) do + { + "id"=>21, + "cryptocurrency"=>"BTC", + "amount"=>"1.1", + "comment"=>"gift from drew", + "link"=>{"telegram"=>"https://t.me/BTC_STAGE_BOT?start=b_9ac6b97e09ecbbfc0d365421f6b98a33", "web"=>"https://s-www.lgk.one/p2p/?start=b_9ac6b97e09ecbbfc0d365421f6b98a33"}, + "createdAt"=>1615444044115, + "expiryAt"=>1615530444115, + "completedAt"=>nil, + "status"=>"active" + } + end + + it 'should create an invoice' do + stub_request(:post, uri + '/api/gate/v1/invoices/') + .with(body: {"cryptocurrency":"BTC","amount":123,"comment":"Exchange service deposit for account uid12312"}.to_json) + .to_return(body: response.to_json, headers: { 'Content-Type': 'application/json' }) + + result = wallet.create_deposit_intention!(comment: 'Exchange service deposit for account uid12312', amount: 123) + + expect(result[:id]).to eq '0:21' + expect(result[:amount]).to eq 1.1 + expect(result[:links]).to be_a(Array) + expect(result[:links].count).to eq(2) + expect(result[:links].first.keys).to eq(['title', 'url']) + expect(result[:expires_at]).to be_a(Time) + end + end + end +end diff --git a/spec/services/wallet_service_spec.rb b/spec/services/wallet_service_spec.rb index 68696fb2f0..2cdaad5b89 100644 --- a/spec/services/wallet_service_spec.rb +++ b/spec/services/wallet_service_spec.rb @@ -26,6 +26,54 @@ Blockchain.any_instance.stubs(:blockchain_api).returns(BlockchainService.new(blockchain)) end + context :create_deposit_intention! do + let(:member) { create(:member) } + let(:amount) { 1.12 } + let(:intention) { { id: 123, amount: amount, links: { web: 'somelink', telegram: 'somelink' }, expires_at: 3.days.ago } } + before do + service.adapter.expects(:create_deposit_intention!).returns(intention) + end + + subject do + service.create_deposit_intention!(member, currency, amount) + end + + it 'creates depotion intention' do + expect(subject).to be_a(Deposit) + end + + it 'deposit amount equals to requested' do + expect(subject.amount).to eq amount + end + end + + context :poll_deposits! do + let(:member) { create(:member) } + let(:amount) { 1.12 } + let(:intention_id) { 12 } + let(:username) { 'ivan' } + let(:wallet) { create(:wallet, :fake_hot, settings: { 'save_beneficiary' => true } ) } + + let(:intentions) { [ + { id: intention_id, amount: amount, address: username } + ] } + let!(:deposit) { create :deposit_btc, amount: amount, currency: wallet.currencies.first, intention_id: intention_id } + + before do + service.adapter.expects(:poll_deposits).returns(intentions) + end + + it 'accepts deposit' do + service.poll_deposits! + expect(deposit.reload).to be_accepted + end + + it 'creates beneficiary' do + service.poll_deposits! + expect(deposit.account.member.beneficiaries.count).to eq(1) + end + end + context :create_address! do let(:account) { create(:member, :level_3, :barong).get_account(currency) } let(:blockchain_address) do @@ -740,7 +788,7 @@ let(:fake_wallet_adapter) { FakeWallet.new } let(:service) { WalletService.new(fee_wallet) } - let(:spread_deposit) do + let(:spread_deposit) do [Peatio::Transaction.new(to_address: 'fake-cold', amount: '2.0', currency_id: currency.id)] diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e0b5d0caf4..f5df30dcfa 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -9,6 +9,10 @@ ENV['EVENT_API_JWT_PRIVATE_KEY'] ||= Base64.urlsafe_encode64(OpenSSL::PKey::RSA.generate(2048).to_pem) ENV['PEATIO_JWT_PRIVATE_KEY'] ||= Base64.urlsafe_encode64(OpenSSL::PKey::RSA.generate(2048).to_pem) ENV['WITHDRAW_ADMIN_APPROVE'] = 'true' +ENV['MINIMUM_MEMBER_LEVEL_FOR_DEPOSIT']='3' +ENV['MINIMUM_MEMBER_LEVEL_FOR_WITHDRAW']='3' +ENV['MINIMUM_MEMBER_LEVEL_FOR_TRADING']='3' +ENV['JWT_PUBLIC_KEY']=nil # We remove lib/peatio.rb from LOAD_PATH because of conflict with peatio gem. # lib/peatio.rb is added to LOAD_PATH later after requiring gems.