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

33
.dockerignore Normal file
View File

@@ -0,0 +1,33 @@
# Ignore git
.git
.gitignore
# JetBrains IDE files
.idea
# Ignore docker
Dockerfile
Dockerfile.plugin
.dockerignore
# Ignore CI related files
ci
.travis.yml
# Ignore files for development
.ruby-version
.rspec
.DS_Store
*.md
docs
tmp
log
.god
# Ignore bundler config
/.bundle
# Ignore locally installed gems
/vendor/bundle

553
.drone.yml Normal file
View File

@@ -0,0 +1,553 @@
---
kind: pipeline
type: docker
name: test-mysql-5.7
services:
- name: database
image: mysql:5.7
ports:
- 3306
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
- name: influxdb
image: influxdb:1.7.8
ports:
- 8086
environment:
INFLUXDB_ADMIN_ENABLED: 'true'
- name: rabbitmq
image: rabbitmq:3.8.9
ports:
- 5672
- name: redis
image: redis:4.0
ports:
- 6379
steps:
- name: wait-for-services
image: jwilder/dockerize
commands:
- dockerize -wait tcp://database:3306 -wait tcp://influxdb:8086 -wait tcp://rabbitmq:5672 -wait tcp://redis:6379 -timeout 1m
- name: init-influxdb
image: influxdb:1.7.8
commands:
- cat db/influxdb.sql | influx -host influxdb
# Copy bundler cache from the host machine to your build environment.
# Plugin description: http://plugins.drone.io/drillster/drone-volume-cache
- name: restore-bundle-cache
pull: if-not-exists
image: drillster/drone-volume-cache
settings:
restore: true
mount:
- vendor/bundle
volumes:
- name: bundle-cache
path: /cache
- name: run-rspec
image: ruby:2.6.5
environment:
DATABASE_HOST: database
INFLUXDB_HOST: influxdb
RABBITMQ_HOST: rabbitmq
EVENT_API_RABBITMQ_HOST: rabbitmq
REDIS_URL: redis://redis:6379
RAILS_ENV: test
VAULT_ADDR: ''
VAULT_TOKEN: ''
commands:
- bin/init_config
- gem install bundler:2.1.4
- bundle install --jobs=$(nproc) --retry=3 --path vendor/bundle
- bundle exec rake db:create db:migrate
- LOG_LEVEL=warn bundle exec rspec
- name: rebuild-bundle-cache
pull: if-not-exists
image: drillster/drone-volume-cache
settings:
rebuild: true
mount:
- vendor/bundle
volumes:
- name: bundle-cache
path: /cache
- name: docs-generate
image: quay.io/openware/ruby-node:2.6.5
environment:
DATABASE_HOST: database
INFLUXDB_HOST: influxdb
RABBITMQ_HOST: rabbitmq
EVENT_API_RABBITMQ_HOST: rabbitmq
REDIS_URL: redis://redis:6379
VAULT_TOKEN: ''
commands:
- gem install bundler:2.1.4
- bin/gendocs
when:
branch:
- master
- "*-stable"
volumes:
- name: bundle-cache
host:
path: /tmp/bundle
trigger:
event:
- push
---
kind: pipeline
type: docker
name: test-mariadb-10.3
services:
- name: mariadb
image: mariadb:10.3
ports:
- 3306
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
- name: influxdb
image: influxdb:1.7.8
ports:
- 8086
environment:
INFLUXDB_ADMIN_ENABLED: 'true'
- name: rabbitmq
image: rabbitmq:3.8.9
ports:
- 5672
- name: redis
image: redis:4.0
ports:
- 6379
steps:
- name: wait-for-services
image: jwilder/dockerize
commands:
- dockerize -wait tcp://mariadb:3306 -wait tcp://influxdb:8086 -wait tcp://rabbitmq:5672 -wait tcp://redis:6379 -timeout 1m
- name: init-influxdb
image: influxdb:1.7.8
commands:
- cat db/influxdb.sql | influx -host influxdb
- name: restore-bundle-cache
pull: if-not-exists
image: drillster/drone-volume-cache
settings:
restore: true
mount:
- vendor/bundle
volumes:
- name: bundle-cache
path: /cache
- name: run-rspec
image: ruby:2.6.5
environment:
DATABASE_HOST: "mariadb"
DATABASE_SUPPORT_JSON: "false"
INFLUXDB_HOST: influxdb
RABBITMQ_HOST: rabbitmq
EVENT_API_RABBITMQ_HOST: rabbitmq
REDIS_URL: redis://redis:6379
RAILS_ENV: test
VAULT_ADDR: ''
VAULT_TOKEN: ''
commands:
- bin/init_config
- gem install bundler:2.1.4
- bundle install --jobs=$(nproc) --retry=3 --path vendor/bundle
- bundle exec rake db:create db:migrate
- LOG_LEVEL=warn bundle exec rspec
trigger:
event:
- push
branch:
- master
- "*-stable"
- "fix-db/*"
---
kind: pipeline
type: docker
name: test-mysql-8.0
services:
- name: mysql
image: mysql:8.0
commands:
- /entrypoint.sh --default-authentication-plugin=mysql_native_password
ports:
- 3306
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
- name: influxdb
image: influxdb:1.7.8
ports:
- 8086
environment:
INFLUXDB_ADMIN_ENABLED: 'true'
- name: rabbitmq
image: rabbitmq:3.8.9
ports:
- 5672
- name: redis
image: redis:4.0
ports:
- 6379
steps:
- name: wait-for-services
image: jwilder/dockerize
commands:
- dockerize -wait tcp://mysql:3306 -wait tcp://influxdb:8086 -wait tcp://rabbitmq:5672 -wait tcp://redis:6379 -timeout 1m
- name: init-influxdb
image: influxdb:1.7.8
commands:
- cat db/influxdb.sql | influx -host influxdb
- name: restore-bundle-cache
pull: if-not-exists
image: drillster/drone-volume-cache
settings:
restore: true
mount:
- vendor/bundle
volumes:
- name: bundle-cache
path: /cache
- name: run-rspec
image: ruby:2.6.5
environment:
DATABASE_HOST: "mysql"
INFLUXDB_HOST: influxdb
RABBITMQ_HOST: rabbitmq
EVENT_API_RABBITMQ_HOST: rabbitmq
REDIS_URL: redis://redis:6379
RAILS_ENV: test
VAULT_ADDR: ''
VAULT_TOKEN: ''
commands:
- bin/init_config
- gem install bundler:2.1.4
- bundle install --jobs=$(nproc) --retry=3 --path vendor/bundle
- bundle exec rake db:create db:migrate
- LOG_LEVEL=warn bundle exec rspec
trigger:
event:
- push
branch:
- master
- "*-stable"
- "fix-db/*"
---
kind: pipeline
type: docker
name: test-postgresql-13.0
services:
- name: postgres
image: postgres:13.0
ports:
- 5432
environment:
POSTGRES_PASSWORD: changeme
- name: influxdb
image: influxdb:1.7.8
ports:
- 8086
environment:
INFLUXDB_ADMIN_ENABLED: 'true'
- name: rabbitmq
image: rabbitmq:3.8.9
ports:
- 5672
- name: redis
image: redis:4.0
ports:
- 6379
steps:
- name: wait-for-services
image: jwilder/dockerize
commands:
- dockerize -wait tcp://postgres:5432 -wait tcp://influxdb:8086 -wait tcp://rabbitmq:5672 -wait tcp://redis:6379 -timeout 1m
- name: init-influxdb
image: influxdb:1.7.8
commands:
- cat db/influxdb.sql | influx -host influxdb
- name: restore-bundle-cache
pull: if-not-exists
image: drillster/drone-volume-cache
settings:
restore: true
mount:
- vendor/bundle
volumes:
- name: bundle-cache
path: /cache
- name: run-rspec
image: ruby:2.6.5
environment:
DATABASE_HOST: "postgres"
DATABASE_USER: "postgres"
DATABASE_PASS: "changeme"
DATABASE_ADAPTER: "postgresql"
DATABASE_PORT: "5432"
DATABASE_COLLATION: ""
INFLUXDB_HOST: influxdb
RABBITMQ_HOST: rabbitmq
EVENT_API_RABBITMQ_HOST: rabbitmq
REDIS_URL: redis://redis:6379
RAILS_ENV: test
VAULT_ADDR: ''
VAULT_TOKEN: ''
commands:
- bin/init_config
- gem install bundler:2.1.4
- bundle install --jobs=$(nproc) --retry=3 --path vendor/bundle
- bundle exec rake db:create db:migrate
- LOG_LEVEL=warn bundle exec rspec
trigger:
event:
- push
branch:
- master
- "*-stable"
- "fix-db/*"
---
kind: pipeline
type: docker
name: docker-build-snapshot
steps:
- name: git-tag-snapshot
image: quay.io/openware/sdk-citools:2.6.7
environment:
BRANCH_NAME: master
REPO_NAME: ${DRONE_REPO}
GITHUB_API_KEY:
from_secret: kite_bot_key
commands:
- BUNDLE_GEMFILE=/sdk/Gemfile bundle exec rake --rakefile=/sdk/Rakefile ci:prebuild
- bin/genversion
# Build and push docker image using ".tags" file.
- name: docker-build-and-push
image: plugins/docker
settings:
username:
from_secret: quay_username
password:
from_secret: quay_password
repo: quay.io/openware/peatio
registry: quay.io
# Redeploy on master.devkube.com
- name: devkube-redeploy
image: rubykube/microkube:0.2.0
environment:
WEBHOOK_JWT_SECRET:
from_secret: devkube_webhook_secret
commands:
- export latest_image=quay.io/openware/peatio:$(cat .tags)
- cd /home/app
- bundle exec rake payload:send[peatio,$latest_image,http://master.devkube.com:1337]
detach: true
when:
branch:
- master
# Update changelog and API documentation, then push it to sdk repository and notify.
- name: push-and-notify
image: quay.io/openware/sdk-citools:2.6.7
environment:
BOT_USERNAME: kite-bot
BOT_NAME: Kite Bot
BOT_EMAIL: kite-bot@heliostech.fr
REPO_NAME: ${DRONE_REPO}
BRANCH_NAME: ${DRONE_BRANCH}
SDK_BRANCH: ${DRONE_BRANCH}
TELEGRAM_BOT_TOKEN:
from_secret: telegram_bot_token
TELEGRAM_CHAT_ID:
from_secret: telegram_chat_id
SLACK_TOKEN:
from_secret: slack_token
SLACK_CHANNEL:
from_secret: slack_channel
GITHUB_API_KEY:
from_secret: kite_bot_key
commands:
- BUNDLE_GEMFILE=/sdk/Gemfile bundle exec rake --rakefile=/sdk/Rakefile ci:postbuild[/drone/src]
when:
branch:
- master
# Trigger custom Peatio image build
- name: build-custom-image
image: curlimages/curl
environment:
SERVER: https://ci.openware.work
REPO: openware/assembly-line
BRANCH: master
COMPONENT: peatio
DRONE_TOKEN:
from_secret: drone_token
commands:
- COMPONENT_VERSION=$(cat .tags)
- "curl -s -XPOST -H \"Authorization: Bearer $DRONE_TOKEN\" -i \"$SERVER/api/repos/$REPO/builds?branch=$BRANCH&ASSEMBLY_COMPONENT_VERSION=$COMPONENT_VERSION&ASSEMBLY_COMPONENT=$COMPONENT\""
detach: true
when:
branch:
- master
trigger:
event:
- push
branch:
- master
- "fix/*"
- "integration/*"
image_pull_secrets:
- dockerconfigjson
---
kind: pipeline
type: docker
name: docker-build-stable
steps:
# Update application version
- name: git-tag-stable
image: quay.io/openware/sdk-citools:2.6.7
environment:
BOT_USERNAME: kite-bot
BOT_NAME: Kite Bot
BOT_EMAIL: kite-bot@heliostech.fr
BRANCH_NAME: ${DRONE_BRANCH}
REPO_NAME: ${DRONE_REPO}
GITHUB_API_KEY:
from_secret: kite_bot_key
commands:
- BUNDLE_GEMFILE=/sdk/Gemfile bundle exec rake --rakefile=/sdk/Rakefile ci:prebuild
- bin/genversion
# Build and push docker image using ".tags" file.
- name: docker-build-and-push
image: plugins/docker
settings:
username:
from_secret: quay_username
password:
from_secret: quay_password
repo: quay.io/openware/peatio
registry: quay.io
# Redeploy on master.devkube.com
- name: devkube-redeploy
image: rubykube/microkube:0.2.0
environment:
WEBHOOK_JWT_SECRET:
from_secret: devkube_webhook_secret
commands:
- export latest_image=quay.io/openware/peatio:$(cat .tags)
- cd /home/app
- bundle exec rake payload:send[peatio,$latest_image,http://master.devkube.com:1337]
detach: true
# Update changelog and API documentation, then push it to sdk repository and notify.
- name: push-and-notify
image: quay.io/openware/sdk-citools:2.6.7
environment:
BOT_USERNAME: kite-bot
BOT_NAME: Kite Bot
BOT_EMAIL: kite-bot@heliostech.fr
REPO_NAME: ${DRONE_REPO}
BRANCH_NAME: ${DRONE_BRANCH}
SDK_BRANCH: ${DRONE_BRANCH}
TELEGRAM_BOT_TOKEN:
from_secret: telegram_bot_token
TELEGRAM_CHAT_ID:
from_secret: telegram_chat_id
SLACK_TOKEN:
from_secret: slack_token
SLACK_CHANNEL:
from_secret: slack_channel
GITHUB_API_KEY:
from_secret: kite_bot_key
commands:
- BUNDLE_GEMFILE=/sdk/Gemfile bundle exec rake --rakefile=/sdk/Rakefile ci:postbuild[/drone/src]
# Trigger custom Peatio image build
- name: build-custom-image
image: curlimages/curl
environment:
SERVER: https://ci.openware.work
REPO: openware/assembly-line
BRANCH: master
COMPONENT: peatio
DRONE_TOKEN:
from_secret: drone_token
commands:
- COMPONENT_VERSION=$(cat .tags)
- "curl -s -XPOST -H \"Authorization: Bearer $DRONE_TOKEN\" -i \"$SERVER/api/repos/$REPO/builds?branch=$BRANCH&ASSEMBLY_COMPONENT_VERSION=$COMPONENT_VERSION&ASSEMBLY_COMPONENT=$COMPONENT\""
detach: true
- name: "Update global OpenDAX version"
image: quay.io/openware/goci:0.0.6
environment:
GIT_TOKEN:
from_secret: kite_bot_key
commands:
- /app/goci versions
trigger:
event:
- push
branch:
- "*-stable"
depends_on:
- test-mysql-5.7
- test-mariadb-10.3
- test-mysql-8.0
- test-postgresql-13.0
image_pull_secrets:
- dockerconfigjson

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.sh text eol=lf

61
.gitignore vendored Normal file
View File

@@ -0,0 +1,61 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile ~/.gitignore_global
# Ignore bundler config
/.bundle
# Ignore the default SQLite database.
/db/*.sqlite3
# Ignore uploaded files
/public/uploads
# Ignore all logfiles and tempfiles.
log
*.log
tmp
# Ignore all config files
/config/application.yml
/config/markets.yml
/config/markets.old.yml
/config/deposit_channels.old.yml
/config/withdraw_channels.old.yml
/config/management_api_v1.yml
/config/secrets
/config/seed
/config/bench/*.yml
# Ignore translations.js
/public/javascripts/translations.js
# Ignore ctags
.tags
tags
# Ignore vim temporary files
**/*.swp
**/*.swo
# Ignore RubyMine files
.idea
/vendor/bundle
.env
.DS_Store
.agignore
**/*.tgz
.cache
.yarnrc
.byebug_history
.generators
.rakeTasks
# Ignore bench reports & logs
bench/reports/*
bench/log/*
/cofing/bench

2
.rspec Normal file
View File

@@ -0,0 +1,2 @@
--color
--require spec_helper

1
.ruby-version Normal file
View File

@@ -0,0 +1 @@
ruby-2.6.6

5511
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

28
Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
# syntax=docker/dockerfile:1.4
# Fast app image — gems come from deps base (custom/dena-deps:1).
# Rebuild deps when Gemfile.lock changes: docker build -f Dockerfile.deps -t custom/dena-deps:1 .
ARG DEPS_IMAGE=custom/dena-deps:1
FROM ${DEPS_IMAGE} AS base
ARG RAILS_ENV=production
ENV RAILS_ENV=${RAILS_ENV} \
APP_HOME=/home/app
USER app
WORKDIR $APP_HOME
COPY --chown=app:app . .
RUN echo "# This file was overridden by default during docker image build." > Gemfile.plugin \
&& ruby ./bin/init_config \
&& chmod +x ./bin/logger \
&& bundle exec rake tmp:create
EXPOSE 3000
CMD ["bundle", "exec", "puma", "--config", "config/puma.rb"]
FROM base
COPY --chown=app:app Gemfile.plugin Gemfile.lock $APP_HOME/
RUN bundle install --path /opt/vendor/bundle --jobs $(nproc)

37
Dockerfile.deps Normal file
View File

@@ -0,0 +1,37 @@
# syntax=docker/dockerfile:1.4
# Heavy layer: Ruby gems + system packages. Rebuild only when Gemfile.lock changes.
# docker build -f Dockerfile.deps -t custom/dena-deps:1 .
FROM ruby:2.7.8
ARG UID=1000
ARG GID=1000
ENV KAIGARA_VERSION=0.1.9
ENV APP_HOME=/home/app \
BUNDLE_PATH=/opt/vendor/bundle \
TZ=UTC
RUN groupadd -r --gid ${GID} app \
&& useradd --system --create-home --home ${APP_HOME} --shell /sbin/nologin --no-log-init \
--gid ${GID} --uid ${UID} app
WORKDIR $APP_HOME
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get install -y --no-install-recommends default-libmysqlclient-dev git curl vim \
&& rm -rf /var/lib/apt/lists/* \
&& gem install bundler:2.2.16 \
&& curl -fsSL -o /usr/bin/kaigara "https://github.com/openware/kaigara/releases/download/${KAIGARA_VERSION}/kaigara" \
&& chmod +x /usr/bin/kaigara
COPY Gemfile Gemfile.lock ./
RUN mkdir -p ${BUNDLE_PATH} \
&& chown -R app:app /opt/vendor ${APP_HOME} \
&& su app -s /bin/bash -c "bundle config set --local path ${BUNDLE_PATH} \
&& bundle install --jobs $(nproc)"
USER app
ENV PATH="${BUNDLE_PATH}/bin:${PATH}"

8
Dockerfile.plugin Normal file
View File

@@ -0,0 +1,8 @@
# Extend base image with plugins.
FROM rubykube/peatio:latest
# Copy Gemfile.plugin for installing plugins.
COPY --chown=app:app Gemfile.plugin $APP_HOME
# Install plugins.
RUN bundle install --path /opt/vendor/bundle

103
Gemfile Normal file
View File

@@ -0,0 +1,103 @@
# encoding: UTF-8
# frozen_string_literal: true
source 'https://rubygems.org'
git_source(:github) { |repo_slug| "https://github.com/#{repo_slug}" }
ruby '~> 2.7'
gem 'ransack', '~> 2.3.2'
gem 'rails', '~> 5.2.4'
gem 'puma', '~> 3.12.2'
gem 'mysql2', '~> 0.5.2'
gem 'redis', '~> 4.1.2', require: ['redis', 'redis/connection/hiredis']
gem 'hiredis', '~> 0.6.0'
gem 'figaro', '~> 1.1.1'
gem 'hashie', '~> 3.6.0'
gem 'aasm', '~> 5.0.8'
gem 'bunny', '~> 2.14.1'
gem 'cancancan', '~> 3.1.0'
gem 'enumerize', '~> 2.2.2'
gem 'kaminari', '~> 1.2.1'
gem 'rbtree', '~> 0.4.2'
gem 'grape', '~> 1.3.1'
gem 'grape-entity', '~> 0.7.1'
gem 'grape-swagger', '~> 0.30.1'
gem 'grape-swagger-ui', '~> 2.2.8'
gem 'grape-swagger-entity', '~> 0.2.5'
gem 'grape_logging', '~> 1.8.0'
gem 'rack-attack', '~> 5.4.2'
gem 'faraday', '~> 0.17'
gem 'better-faraday', '~> 1.0.5'
gem 'faraday_middleware', '~> 0.13.1'
gem 'faye', '~> 1.4'
gem 'eventmachine', '~> 1.2'
gem 'em-synchrony', '~> 1.0'
gem 'jwt', '~> 2.2.0'
gem 'email_validator', '~> 1.6.0'
gem 'validate_url', '~> 1.0.4'
gem 'god', '~> 0.13.7', require: false
gem 'arel-is-blank', '~> 1.0.0'
gem 'sentry-raven', '~> 2.9.0', require: false
gem 'memoist', '~> 0.16.0'
gem 'method-not-implemented', '~> 1.0.1'
gem 'validates_lengths_from_database', '~> 0.7.0'
gem 'jwt-multisig', '~> 1.0.0'
gem 'cash-addr', '~> 0.2.0', require: 'cash_addr'
gem 'digest-sha3', '~> 1.1.0'
gem 'scout_apm', '~> 2.4', require: false
gem 'peatio', '~> 2.6.2'
gem 'irix', '~> 2.6.0'
gem 'rack-cors', '~> 1.0.6', require: false
gem 'jwt-rack', '~> 0.1.0', require: false
gem 'env-tweaks', '~> 1.0.0'
gem 'vault', '~> 0.12', require: false
gem 'vault-rails', git: 'https://github.com/rubykube/vault-rails'
gem 'bootsnap', '>= 1.1.0', require: false
gem 'net-http-persistent', '~> 3.0.1'
gem 'influxdb', '~> 0.7.0'
gem 'safe_yaml', '~> 1.0.5', require: 'safe_yaml/load'
gem 'progress_bar'
gem 'thread'
gem 'dotenv-rails'
gem 'rails_pdate'
group :development, :test do
gem 'irb'
gem 'bump', '~> 0.7'
gem 'faker', '~> 1.8'
gem 'pry-byebug', '~> 3.7'
gem 'bullet', '~> 5.9'
gem 'grape_on_rails_routes', '~> 0.3.2'
end
group :development do
gem 'annotate', '~> 3.1.0'
gem 'ruby-prof', '~> 0.17.0', require: false
gem 'listen', '>= 3.0.5', '< 3.2'
end
group :test do
gem 'rspec-rails', '~> 3.8', '>= 3.8.2'
gem 'rspec-retry', '~> 0.6'
gem 'webmock', '~> 3.5'
gem 'database_cleaner', '~> 1.7'
gem 'mocha', '~> 1.8', require: false
gem 'factory_bot_rails', '~> 5.0', '>= 5.0.2'
gem 'timecop', '~> 0.9'
gem 'rubocop-rspec', '~> 1.32', require: false
end
# Load gems from Gemfile.plugin.
Dir.glob File.expand_path('../Gemfile.plugin', __FILE__) do |file|
eval_gemfile file
end
gem "pg", "~> 1.2"
# Security patches — see Docs/10-cve-audit.md (phase 1)
gem 'rack', '~> 2.2.13'
gem 'nokogiri', '~> 1.15.5'
gem 'rexml', '>= 3.3.9'
gem 'tzinfo', '~> 1.2.10'
gem 'rails-html-sanitizer', '>= 1.4.4'

557
Gemfile.lock Normal file
View File

@@ -0,0 +1,557 @@
GIT
remote: https://github.com/rubykube/vault-rails
revision: 167283b25c34ba8a61ce2ad505f68dced3189f43
specs:
vault-rails (0.6.0)
rails (>= 4.1)
vault (~> 0.14)
GEM
remote: https://rubygems.org/
specs:
aasm (5.0.8)
concurrent-ruby (~> 1.0)
actioncable (5.2.5)
actionpack (= 5.2.5)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailer (5.2.5)
actionpack (= 5.2.5)
actionview (= 5.2.5)
activejob (= 5.2.5)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.2.5)
actionview (= 5.2.5)
activesupport (= 5.2.5)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.5)
activesupport (= 5.2.5)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
activejob (5.2.5)
activesupport (= 5.2.5)
globalid (>= 0.3.6)
activemodel (5.2.5)
activesupport (= 5.2.5)
activerecord (5.2.5)
activemodel (= 5.2.5)
activesupport (= 5.2.5)
arel (>= 9.0)
activestorage (5.2.5)
actionpack (= 5.2.5)
activerecord (= 5.2.5)
marcel (~> 1.0.0)
activesupport (5.2.5)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
amq-protocol (2.3.2)
amqp (1.8.0)
amq-protocol (>= 2.2.0)
eventmachine
annotate (3.1.1)
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0)
arel (9.0.0)
arel-is-blank (1.0.0)
arel (>= 6.0)
arel-to-sql (~> 1.0)
arel-to-sql (1.0.0)
arel (>= 6.0)
ast (2.4.2)
aws-eventstream (1.1.1)
aws-sigv4 (1.2.3)
aws-eventstream (~> 1, >= 1.0.2)
base58 (0.2.3)
better-faraday (1.0.8)
activesupport (>= 4.0, < 6.0)
faraday (~> 0.12)
bootsnap (1.7.4)
msgpack (~> 1.0)
builder (3.2.4)
bullet (5.9.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
bump (0.10.0)
bunny (2.14.4)
amq-protocol (~> 2.3, >= 2.3.0)
byebug (11.1.3)
cancancan (3.1.0)
cash-addr (0.2.0)
base58 (~> 0.2.2)
clamp (1.3.2)
coderay (1.1.3)
concurrent-ruby (1.1.8)
connection_pool (2.2.5)
cookiejar (0.3.3)
crack (0.4.5)
rexml
crass (1.0.7)
daemons (1.4.0)
database_cleaner (1.99.0)
diff-lcs (1.4.4)
digest-sha3 (1.1.0)
dotenv (2.7.6)
dotenv-rails (2.7.6)
dotenv (= 2.7.6)
railties (>= 3.2)
dry-configurable (0.12.1)
concurrent-ruby (~> 1.0)
dry-core (~> 0.5, >= 0.5.0)
dry-container (0.7.2)
concurrent-ruby (~> 1.0)
dry-configurable (~> 0.1, >= 0.1.3)
dry-core (0.5.0)
concurrent-ruby (~> 1.0)
dry-inflector (0.2.0)
dry-logic (1.2.0)
concurrent-ruby (~> 1.0)
dry-core (~> 0.5, >= 0.5)
dry-types (1.5.1)
concurrent-ruby (~> 1.0)
dry-container (~> 0.3)
dry-core (~> 0.5, >= 0.5)
dry-inflector (~> 0.1, >= 0.1.2)
dry-logic (~> 1.0, >= 1.0.2)
em-http-request (1.1.7)
addressable (>= 2.3.4)
cookiejar (!= 0.3.1)
em-socksify (>= 0.3)
eventmachine (>= 1.0.3)
http_parser.rb (>= 0.6.0)
em-socksify (0.3.2)
eventmachine (>= 1.0.0.beta.4)
em-synchrony (1.0.6)
eventmachine (>= 1.0.0.beta.1)
em-websocket (0.5.2)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
email_validator (1.6.0)
activemodel
enumerize (2.2.2)
activesupport (>= 3.2)
env-tweaks (1.0.1)
activesupport (>= 3.0, < 7.0)
erubi (1.10.0)
eventmachine (1.2.7)
factory_bot (5.2.0)
activesupport (>= 4.2.0)
factory_bot_rails (5.2.0)
factory_bot (~> 5.2.0)
railties (>= 4.2.0)
faker (1.9.6)
i18n (>= 0.7)
faraday (0.17.4)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
faye (1.4.0)
cookiejar (>= 0.3.0)
em-http-request (>= 1.1.6)
eventmachine (>= 0.12.0)
faye-websocket (>= 0.11.0)
multi_json (>= 1.0.0)
rack (>= 1.0.0)
websocket-driver (>= 0.5.1)
faye-websocket (0.11.0)
eventmachine (>= 0.12.0)
websocket-driver (>= 0.5.1)
ffi (1.15.0)
figaro (1.1.1)
thor (~> 0.14)
globalid (0.4.2)
activesupport (>= 4.2.0)
god (0.13.7)
grape (1.3.3)
activesupport
builder
dry-types (>= 1.1)
mustermann-grape (~> 1.0.0)
rack (>= 1.3.0)
rack-accept
grape-entity (0.7.1)
activesupport (>= 4.0)
multi_json (>= 1.3.2)
grape-swagger (0.30.1)
grape (>= 0.16.2)
grape-swagger-entity (0.2.5)
grape-entity (>= 0.5.0)
grape-swagger (>= 0.20.4)
grape-swagger-ui (2.2.8)
railties (>= 3.1)
grape_logging (1.8.4)
grape
rack
grape_on_rails_routes (0.3.2)
rails (>= 3.1.1)
hashdiff (1.0.1)
hashie (3.6.0)
highline (2.0.3)
hiredis (0.6.3)
http_parser.rb (0.6.0)
i18n (1.8.10)
concurrent-ruby (~> 1.0)
influxdb (0.7.0)
io-console (0.5.9)
irb (1.3.5)
reline (>= 0.1.5)
irix (2.6.0)
em-synchrony (~> 1.0)
em-websocket
eventmachine
faraday_middleware (~> 0.13.1)
faye (~> 1.2)
peatio (>= 2.4.2)
jwt (2.2.3)
jwt-multisig (1.0.5)
activesupport (>= 4.0)
jwt (~> 2.2)
jwt-rack (0.1.1)
jwt (~> 2.2)
rack
kaminari (1.2.1)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.1)
kaminari-activerecord (= 1.2.1)
kaminari-core (= 1.2.1)
kaminari-actionview (1.2.1)
actionview
kaminari-core (= 1.2.1)
kaminari-activerecord (1.2.1)
activerecord
kaminari-core (= 1.2.1)
kaminari-core (1.2.1)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
loofah (2.25.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.7.1)
mini_mime (>= 0.1.1)
marcel (1.0.1)
memoist (0.16.2)
method-not-implemented (1.0.1)
method_source (1.0.0)
mini_mime (1.1.0)
minitest (5.14.4)
mocha (1.12.0)
msgpack (1.4.2)
multi_json (1.15.0)
multipart-post (2.1.1)
mustermann (1.1.1)
ruby2_keywords (~> 0.0.1)
mustermann-grape (1.0.1)
mustermann (>= 1.0.0)
mysql2 (0.5.3)
net-http-persistent (3.0.1)
connection_pool (~> 2.2)
nio4r (2.5.7)
nokogiri (1.15.7-x86_64-linux)
racc (~> 1.4)
options (2.3.2)
parallel (1.20.1)
parser (3.0.1.1)
ast (~> 2.4.1)
peatio (2.6.3)
activemodel (> 5.2, <= 6.0.0)
amqp
bunny
clamp
em-synchrony (~> 1.0)
em-websocket
eventmachine
faraday_middleware (~> 0.13.1)
faye (~> 1.2)
jwt
mysql2
prometheus-client
thin
peatio-bitcoincash (2.6.1)
activesupport (~> 5.2.3)
better-faraday (~> 1.0.5)
cash-addr (~> 0.2.0)
faraday (~> 0.17)
memoist (~> 0.16.0)
net-http-persistent (~> 3.0.1)
peatio (>= 0.6.3)
peatio-bitgo (2.6.6)
activesupport (~> 5.2.3)
better-faraday (~> 1.0.5)
faraday (~> 0.17)
memoist (~> 0.16.0)
net-http-persistent (~> 3.0.1)
peatio (>= 0.6.3)
peatio-dash (2.6.1)
activesupport (~> 5.2.3)
better-faraday (~> 1.0.5)
faraday (~> 0.17)
memoist (~> 0.16.0)
net-http-persistent (~> 3.0.1)
peatio (>= 0.6.3)
peatio-electrum (2.6.2)
activesupport (~> 5.2.3)
faraday (~> 0.17)
net-http-persistent (~> 3.0.1)
peatio (>= 0.6.3)
peatio-litecoin (2.6.1)
activesupport (~> 5.2.3)
better-faraday (~> 1.0.5)
faraday (~> 0.17)
memoist (~> 0.16.0)
net-http-persistent (~> 3.0.1)
peatio (>= 0.6.3)
peatio-ripple (2.6.1)
activesupport (~> 5.2.3)
better-faraday (~> 1.0.5)
faraday (~> 0.17)
memoist (~> 0.16.0)
net-http-persistent (~> 3.0.1)
peatio (>= 0.6.3)
pg (1.2.3)
polyamorous (2.3.2)
activerecord (>= 5.2.1)
progress_bar (1.3.3)
highline (>= 1.6, < 3)
options (~> 2.3.0)
prometheus-client (2.1.0)
pry (0.13.1)
coderay (~> 1.1)
method_source (~> 1.0)
pry-byebug (3.9.0)
byebug (~> 11.0)
pry (~> 0.13.0)
public_suffix (4.0.6)
puma (3.12.6)
racc (1.8.1)
rack (2.2.23)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (5.4.2)
rack (>= 1.0, < 3)
rack-cors (1.0.6)
rack (>= 1.6.0)
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (5.2.5)
actioncable (= 5.2.5)
actionmailer (= 5.2.5)
actionpack (= 5.2.5)
actionview (= 5.2.5)
activejob (= 5.2.5)
activemodel (= 5.2.5)
activerecord (= 5.2.5)
activestorage (= 5.2.5)
activesupport (= 5.2.5)
bundler (>= 1.3.0)
railties (= 5.2.5)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.5.0)
loofah (~> 2.19, >= 2.19.1)
rails_pdate (0.0.4)
rails (>= 4.0.0)
railties (5.2.5)
actionpack (= 5.2.5)
activesupport (= 5.2.5)
method_source
rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0)
rainbow (3.0.0)
rake (13.0.3)
ransack (2.3.2)
activerecord (>= 5.2.1)
activesupport (>= 5.2.1)
i18n
polyamorous (= 2.3.2)
rb-fsevent (0.10.4)
rb-inotify (0.10.1)
ffi (~> 1.0)
rbtree (0.4.4)
redis (4.1.4)
regexp_parser (2.1.1)
reline (0.2.5)
io-console (~> 0.5)
rexml (3.4.4)
rspec-core (3.9.3)
rspec-support (~> 3.9.3)
rspec-expectations (3.9.4)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-rails (3.9.1)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
rspec-mocks (~> 3.9.0)
rspec-support (~> 3.9.0)
rspec-retry (0.6.2)
rspec-core (> 3.3)
rspec-support (3.9.4)
rubocop (0.93.1)
parallel (~> 1.10)
parser (>= 2.7.1.5)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8)
rexml
rubocop-ast (>= 0.6.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-ast (1.5.0)
parser (>= 3.0.1.1)
rubocop-rspec (1.44.1)
rubocop (~> 0.87)
rubocop-ast (>= 0.7.1)
ruby-prof (0.17.0)
ruby-progressbar (1.11.0)
ruby2_keywords (0.0.4)
ruby_dep (1.5.0)
safe_yaml (1.0.5)
scout_apm (2.6.10)
parser
sentry-raven (2.9.0)
faraday (>= 0.7.6, < 1.0)
sprockets (4.0.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.2)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
thin (1.8.0)
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3)
thor (0.20.3)
thread (0.2.2)
thread_safe (0.3.6)
timecop (0.9.4)
tzinfo (1.2.11)
thread_safe (~> 0.1)
unicode-display_width (1.7.0)
uniform_notifier (1.14.2)
validate_url (1.0.13)
activemodel (>= 3.0.0)
public_suffix
validates_lengths_from_database (0.7.0)
activerecord (>= 3)
vault (0.16.0)
aws-sigv4
webmock (3.12.2)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
websocket-driver (0.7.3)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
PLATFORMS
x86_64-linux
DEPENDENCIES
aasm (~> 5.0.8)
annotate (~> 3.1.0)
arel-is-blank (~> 1.0.0)
better-faraday (~> 1.0.5)
bootsnap (>= 1.1.0)
bullet (~> 5.9)
bump (~> 0.7)
bunny (~> 2.14.1)
cancancan (~> 3.1.0)
cash-addr (~> 0.2.0)
database_cleaner (~> 1.7)
digest-sha3 (~> 1.1.0)
dotenv-rails
em-synchrony (~> 1.0)
email_validator (~> 1.6.0)
enumerize (~> 2.2.2)
env-tweaks (~> 1.0.0)
eventmachine (~> 1.2)
factory_bot_rails (~> 5.0, >= 5.0.2)
faker (~> 1.8)
faraday (~> 0.17)
faraday_middleware (~> 0.13.1)
faye (~> 1.4)
figaro (~> 1.1.1)
god (~> 0.13.7)
grape (~> 1.3.1)
grape-entity (~> 0.7.1)
grape-swagger (~> 0.30.1)
grape-swagger-entity (~> 0.2.5)
grape-swagger-ui (~> 2.2.8)
grape_logging (~> 1.8.0)
grape_on_rails_routes (~> 0.3.2)
hashie (~> 3.6.0)
hiredis (~> 0.6.0)
influxdb (~> 0.7.0)
irb
irix (~> 2.6.0)
jwt (~> 2.2.0)
jwt-multisig (~> 1.0.0)
jwt-rack (~> 0.1.0)
kaminari (~> 1.2.1)
listen (>= 3.0.5, < 3.2)
memoist (~> 0.16.0)
method-not-implemented (~> 1.0.1)
mocha (~> 1.8)
mysql2 (~> 0.5.2)
net-http-persistent (~> 3.0.1)
nokogiri (~> 1.15.5)
peatio (~> 2.6.2)
peatio-bitcoincash (~> 2.6.1)
peatio-bitgo (~> 2.6.3)
peatio-dash (~> 2.6.1)
peatio-electrum (~> 2.6.2)
peatio-litecoin (~> 2.6.1)
peatio-ripple (~> 2.6.1)
pg (~> 1.2)
progress_bar
pry-byebug (~> 3.7)
puma (~> 3.12.2)
rack (~> 2.2.13)
rack-attack (~> 5.4.2)
rack-cors (~> 1.0.6)
rails (~> 5.2.4)
rails-html-sanitizer (>= 1.4.4)
rails_pdate
ransack (~> 2.3.2)
rbtree (~> 0.4.2)
redis (~> 4.1.2)
rexml (>= 3.3.9)
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)
sentry-raven (~> 2.9.0)
thread
timecop (~> 0.9)
tzinfo (~> 1.2.10)
validate_url (~> 1.0.4)
validates_lengths_from_database (~> 0.7.0)
vault (~> 0.12)
vault-rails!
webmock (~> 3.5)
RUBY VERSION
ruby 2.7.8p225
BUNDLED WITH
2.2.16

11
Gemfile.plugin Normal file
View File

@@ -0,0 +1,11 @@
source 'https://rubygems.org'
git_source(:github) { |repo_slug| "https://github.com/#{repo_slug}" }
# List your plugins here.
gem 'peatio-dash', '~> 2.6.1'
gem 'peatio-litecoin', '~> 2.6.1'
gem 'peatio-bitcoincash', '~> 2.6.1'
gem 'peatio-ripple', '~> 2.6.1'
gem 'peatio-bitgo', '~> 2.6.3'
gem 'peatio-electrum', '~> 2.6.2'

25
LICENSE.md Normal file
View File

@@ -0,0 +1,25 @@
The MIT License (MIT)
=====================
Copyright © `2018` `Helios Technologies`
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the “Software”), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

261
README.md Normal file
View File

@@ -0,0 +1,261 @@
<div dir="rtl">
# Dena — موتور معاملات Fibitex (Peatio)
**Dena** فورک [Peatio](https://github.com/openware/peatio) `2-6-stable` برای صرافی Fibitex.
| OpenDAX | Fibitex |
|---------|---------|
| Peatio | **Dena** |
| `/api/v2/peatio` | `/api/v2/dena` |
## پیش‌نیازها
Ruby 2.6.6، Rails 5.2، MySQL، Redis، RabbitMQ — معمولاً via Alvand-P Docker.
## راه‌اندازی سریع
<div dir="ltr">
```bash
# توصیه: از Alvand-P
cd ../Alvand-P && rake service:all
# یا standalone: ./bin/setup && rails s
```
</div>
لوکال: [`Docs/06-setup-local.md`](../Docs/06-setup-local.md) · Staging: [`Docs/Fibitex-staging-deploy.md`](../Docs/Fibitex-staging-deploy.md)
## مستندات
| سند | موضوع |
|-----|--------|
| [`Docs/02-dena-microservice.md`](../Docs/02-dena-microservice.md) | معماری Dena |
| [`Docs/README.md`](../Docs/README.md) | فهرست مرکزی |
| [`Alvand-P/README.md`](../Alvand-P/README.md) | Deploy compose |
| [`carbon_platform/README.md`](../carbon_platform/README.md) | Market making |
</div>
---
### Peatio - Cryptocurrency Exchange Software
[![Build Status](https://ci.openware.work/api/badges/openware/peatio/status.svg)](https://ci.openware.work/openware/peatio)
[![Telegram Chat](https://cdn.rawgit.com/Patrolavia/telegram-badge/8fe3382b/chat.svg)](https://t.me/peatio)
## What is Peatio
Peatio is a free and open-source crypto-currency exchange implementation with the Rails framework.
This is a fork of Peatio designed for micro-services architecture. We have simplified the code
in order to use only Peatio API with external frontend and server components.
Peatio is the core accounting component and configuration for markets; it is part of OpenDAX system.
## Getting Started
OpenDAX is a container distribution, the fastest way to install the full stack is using [OpenDAX](https://github.com/openware/opendax)
OpenDAX can be installed under 15 minutes on any Linux / Mac OS X environment with Docker.
```
# To install
git clone https://github.com/openware/opendax.git
# Follow the README instructions
# Configure config/app.yml
bundle install
bundle exec rake service:all
# Open your browser on www.app.local (please it in /etc/hosts)
```
To build your own exchange you should now run Peatio as a backend instead of forking the repository,
and extend it using other microservices such as [Barong](https://www.github.com/rubykube/barong).
## System Overview
![Cryptocurrency Exchange Platform overview](https://github.com/openware/meta/raw/main/images/system.png)
This is a service oriented architecture; the system is designed to be customized by creating Applogic which is your api code.
Barong will dispatch the traffic on your api to extend the current system.
## Mission
Our mission is to build an open-source [crypto exchange software](https://www.openware.com) with a high performance trading engine and incomparable security. We are moving toward dev/ops best practices of running an enterprise grade exchange.
We provide webinar or on site training for installing, configuring and administration best practices of Peatio.
Feel free to contact us for joining the next training session: [Openware.com](https://www.openware.com)
Help is greatly appreciated, feel free to submit pull-requests or open issues.
## Things You Should Know
**RUNNING A CRYPTO CURRENCY EXCHANGE IS HARD.**
This repository is not a turn key solution and will require engineering and design of security process by your company, with or without our assistance. This repository is one component among many we recommend using for composing an enterprise grade exchange. It is highly recommended to deploy a UAT environment and build automated tests for your needs, including Functional tests, Smoke tests and Security vulnerability scans. You may not need to have an active developer on Peatio source code, however, we recommend the following team setup: 1 dev/ops, 3 frontend developers (react / angular), 2 QA engineers, 1 Security Officer.
**SECURITY KNOWLEDGE IS A REQUIREMENT.**
Peatio cannot protect your customers if you leave your admin password 1234567, or open sensitive ports to public internet. No one can. Running an exchange is a very risky task because you're dealing with money directly. If you don't know how to make your exchange secure, hire an expert.
You must know what you're doing, there's no shortcut. Please get prepared before you continue:
* Rails knowledge
* Security knowledge
* Cloud and Linux administration
* Docker and Kubernetes administration
* Micro-services and OAuth 2.0
## Features
* Designed as high performance crypto currency exchange
* Built-in high performance matching-engine
* Built-in multiple wallet support (e.g. deposit, hot, warm and cold)
* Built-in [plugable coin API](docs/coins/development.md)
* Build-in Management API - server-to-server API with high privileges
* Build-in RabbitMQ Event API
* Usability and scalability
* Websocket API and high frequency trading support
* Support multiple digital currencies (e.g. Bitcoin, Litecoin, Ethereum, Ripple etc.)
* Support ERC20 Tokens
* API endpoint for FIAT deposits or payment gateways.
* Powerful admin dashboard and management tools
* Highly configurable and extendable
* Industry standard security out of box
* Maintained by [Openware.com](https://www.openware.com)
* [KYC Verification](http://en.wikipedia.org/wiki/Know_your_customer) provided by [Barong](https://www.github.com/openware/barong)
## Contribute
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute
issues, fixes, and patches to this project.
## Getting Started
We advice to use [minimalistic environment](#minimalistic-local-development-environment-with-docker-compose) if you want to develop only Peatio and don't touch processes which interact with other components.
Otherwise we advice to use [microkube based environment](#local-development-environment-with-microkube)
### Minimalistic local development environment with docker-compose:
#### Prerequisites
* [Docker](https://docs.docker.com/install/) installed
* [Docker compose](https://docs.docker.com/compose/install/) installed
* Ruby 2.6.5
* Rails 5.2.3+
## Installation
### Local development install
1. Set up initial configuration `./bin/setup`
2. Start peatio daemons `god -c lib/daemons/daemons.god`
3. Add this to your `/etc/hosts`:
```
127.0.0.1 www.app.local
127.0.0.1 peatio.app.local
127.0.0.1 barong.app.local
```
4. Start rails server `JWT_PUBLIC_KEY=$(cat config/secrets/rsa-key.pub| base64 -w0) rails s -b 0.0.0.0`
(`base64 -b0` for macOS)
### Local development environment with docker compose:
We suggest you to start using Peatio by installing [OpenDAX](https://github.com/openware/opendax).
[OpenDAX](https://github.com/openware/opendax) which is based on
[Docker containers](https://www.docker.com/what-docker) is a convenient and straightforward way to start
Peatio crypto exchange software development environment.
#### Prerequisites
* [Docker](https://docs.docker.com/install/) installed
* [Docker compose](https://docs.docker.com/compose/install/) installed
#### Start OpenDAX ready to use
Follow [OpenDAX](https://github.com/openware/opendax) documentation for the latest Peatio installation information.
#### [Barong](https://github.com/openware/barong)
Barong is an essential part of Openware [crypto exchange software](https://www.openware.com) stack. It's providing the authentication service, it provides KyC and 2FA features out of the box.
Barong manages roles and kyc level across all applications from the OpenDAX stack. It can be easily extended using Rest Management API and Event API.
##### Barong key features
* KYC Verification for individuals
* SMS and Google two-factor authentication
* Transaction Signature support
* Implement JWT standard to authenticate users of every microservice of the OpenDAX stack
Start barong:
```sh
$> docker-compose run --rm barong bash -c "./bin/link_config && ./bin/setup"
$> docker-compose up -d barong
```
This will output password for **admin@barong.io**. Default password is **`Qwerty123`**
#### Peatio
Start peatio server
```sh
$> docker-compose run --rm peatio bash -c "bundle exec rake db:create db:migrate db:seed"
$> docker-compose up -d peatio
```
After all of that you can start using Peatio in your browser just by following one of the hosts which you added earlier.
## API
You can interact with Peatio through API:
* Account, Market & Public API v2
* Management API v2
* Websocket API
* Event API (AMQP)
## Getting Involved
We want to make it super-easy for Peatio users and contributors to talk to us and connect with each other, to share ideas, solve problems and help make Peatio awesome. Here are the main channels we're running currently, we'd love to hear from you on one of them:
### Discourse
[Rubykube Discourse Forum](https://discuss.rubykube.io)
This is for all Peatio users. You can find guides, recipes, questions, and answers from Snowplow users including the Peatio.tech team.
We welcome questions and contributions!
### Telegram
[@peatio](https://t.me/peatio)
Chat with us and other community members on Telegram.
### GitHub
Peatio issues
If you spot a bug, then please raise an issue in our main GitHub project (Openware Peatio)[https://github.com/openware/peatio/]; likewise, if you have developed a new feature or an improvement in your Rubykube Peatio fork, then send us a pull request!
If you want to brainstorm a potential new feature, then the Telegram group is the best place to start (see above).
### Email
hello@openware.com
If you want to talk directly to us (e.g. about a commercially sensitive issue), email is the easiest way.
## Getting Support and Customization
If you need help with running/deploying/customizing Peatio,
you can contact us on [Openware.com](https://www.openware.com).
Contact us by email: [hello@openware.com](mailto:hello@openware.com)
## License
Peatio is released under the terms of the [MIT license](http://peatio.mit-license.org).
## What is Peatio?
[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.

13
Rakefile Normal file
View File

@@ -0,0 +1,13 @@
# encoding: UTF-8
# frozen_string_literal: true
#!/usr/bin/env rake
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require File.expand_path('../config/application', __FILE__)
Peatio::Application.load_tasks
# Load additional tasks from "support/tasks".
Dir.glob('lib/peatio/tasks/**/*') { |f| load(f) }

1
VERSION Normal file
View File

@@ -0,0 +1 @@
2.6.0

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

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

View File

@@ -0,0 +1,75 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Account
class Balances < Grape::API
helpers ::API::V2::ParamHelpers
# TODO: Add failures.
# TODO: Move desc hash options to block once issues are resolved.
# https://github.com/ruby-grape/grape/issues/1789
# https://github.com/ruby-grape/grape-swagger/issues/705
desc 'Get list of user accounts',
is_array: true,
success: API::V2::Entities::Account
params do
use :pagination
optional :nonzero,
type: { value: Boolean, message: 'account.balances.invalid_nonzero' },
default: false,
desc: 'Filter non zero balances.'
optional :search, type: JSON, default: {} do
optional :currency_code,
as: :code,
type: String
optional :currency_name,
as: :name,
type: String
end
end
get '/balances' do
user_authorize! :read, ::Operations::Account
search_params = params[:search]
.slice(:code, :name)
.transform_keys {|k| "#{k}_cont"}
.merge(m: 'or')
accounts = ::Currency.visible.ransack(search_params).result.each_with_object([]) do |c, result|
account = ::Account.find_by(currency: c, member: current_user)
if account.present?
next if params[:nonzero].present? && account.amount.zero? && account.locked.zero?
result << account
elsif account.blank? && params[:nonzero].blank?
result << ::Account.new(currency: c, member: current_user)
end
end
present paginate(accounts),
with: Entities::Account, current_user: current_user
end
desc 'Get user account by currency' do
success API::V2::Entities::Account
# TODO: Add failures.
end
params do
requires :currency,
type: String,
values: { value: -> { Currency.visible.pluck(:id) }, message: 'account.currency.doesnt_exist' },
desc: 'The currency code.'
end
get '/balances/:currency', requirements: { currency: /[\w\.\-]+/ } do
user_authorize! :read, ::Operations::Account
present current_user.accounts.visible.find_by!(currency_id: params[:currency]),
with: API::V2::Entities::Account
end
end
end
end
end

View File

@@ -0,0 +1,205 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Account
class Beneficiaries < Grape::API
before { withdraws_must_be_permitted! }
namespace :beneficiaries do
desc 'Get list of user beneficiaries',
is_array: true,
success: API::V2::Entities::Beneficiary
params do
optional :currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist' },
as: :currency_id,
desc: 'Beneficiary currency code.'
optional :state,
type: String,
values: { value: -> { ::Beneficiary::STATES_AVAILABLE_FOR_MEMBER.map(&:to_s) }, message: 'account.beneficiary.invalid_state'},
desc: 'Defines either beneficiary active - user can use it to withdraw money'\
'or pending - requires beneficiary activation with pin.'
end
get do
user_authorize! :read, ::Beneficiary
current_user
.beneficiaries
.available_to_member
.tap do |q|
q.where!(currency_id: params[:currency_id]) if params[:currency_id].present?
end
.tap do |q|
q.where!(state: params[:state]) if params[:state].present?
end
.yield_self do |b|
present paginate(b), with: API::V2::Entities::Beneficiary
end
end
desc 'Get beneficiary by ID',
success: API::V2::Entities::Beneficiary
params do
requires :id,
type: { value: Integer, message: 'account.beneficiary.non_integer_id' },
desc: 'Beneficiary Identifier in Database'
end
get ':id' do
user_authorize! :read, ::Beneficiary
current_user
.beneficiaries
.available_to_member
.find_by!(id: params[:id])
.yield_self { |b| present b, with: API::V2::Entities::Beneficiary }
end
desc 'Create new beneficiary',
success: API::V2::Entities::Beneficiary
params do
requires :currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist' },
as: :currency_id,
desc: 'Beneficiary currency code.'
requires :name,
type: String,
allow_blank: false,
values: { value: ->(v) { v.present? && v.size <= 64 }, message: 'account.beneficiary.too_long_name' },
desc: 'Human rememberable name which refer beneficiary.'
optional :description,
type: String,
values: { value: ->(v) { v.size <= 255 }, message: 'account.beneficiary.too_long_description' },
desc: 'Human rememberable name which refer beneficiary.'
requires :data,
type: { value: JSON, message: 'account.beneficiary.non_json_data' },
allow_blank: false,
desc: 'Beneficiary data in JSON format'
end
post do
user_authorize! :create, ::Beneficiary
declared_params = declared(params)
currency = Currency.find_by!(id: params[:currency_id])
if !currency.withdrawal_enabled?
error!({ errors: ['account.currency.withdrawal_disabled'] }, 422)
elsif currency.coin? && declared_params.dig(:data, :address).blank?
error!({ errors: ['account.beneficiary.missing_address_in_data'] }, 422)
elsif currency.fiat? && declared_params.dig(:data, :full_name).blank?
error!({ errors: ['account.beneficiary.missing_full_name_in_data'] }, 422)
end
# Since data is stored in MySQL JSON format we iterate through all
# beneficiaries one by one to detect duplicated address.
if currency.coin? &&
current_user
.beneficiaries
.available_to_member
.where(currency: currency)
.any? { |b| b.data['address'] == declared_params.dig(:data, :address) }
error!({ errors: ['account.beneficiary.duplicate_address'] }, 422)
end
present current_user
.beneficiaries
.create!(declared_params),
with: API::V2::Entities::Beneficiary
rescue ActiveRecord::RecordInvalid => e
report_exception(e)
error!({ errors: ['account.beneficiary.failed_to_create'] }, 422)
end
desc 'Resend beneficiary pin'
params do
requires :id,
type: { value: Integer, message: 'account.beneficiary.non_integer_id' },
desc: 'Beneficiary Identifier in Database'
end
patch ':id/resend_pin' do
user_authorize! :update, ::Beneficiary
beneficiary = current_user
.beneficiaries
.available_to_member
.find_by!(id: params[:id])
unless beneficiary.pending?
error!({ errors: ['account.beneficiary.cant_resend'] }, 422)
end
if Time.now - beneficiary.sent_at < 60
error!({ errors: ['account.beneficiary.cant_resend_within_1_minute'], sent_at: beneficiary.sent_at.iso8601 }, 422)
end
beneficiary.regenerate_pin!
status 204
end
desc 'Activates beneficiary with pin',
success: API::V2::Entities::Beneficiary
params do
requires :id,
type: { value: Integer, message: 'account.beneficiary.non_integer_id' },
desc: 'Beneficiary Identifier in Database'
requires :pin,
type: { value: Integer, message: 'account.beneficiary.non_integer_pin' },
desc: 'Pin code for beneficiary activation'
end
patch ':id/activate' do
user_authorize! :update, ::Beneficiary
beneficiary = current_user
.beneficiaries
.available_to_member
.find_by!(id: params[:id])
unless beneficiary.pending?
error!({ errors: ['account.beneficiary.cant_activate'] }, 422)
end
if beneficiary.activate!(params[:pin])
present beneficiary, with: API::V2::Entities::Beneficiary
else
error!({ errors: ['account.beneficiary.invalid_pin'] }, 422)
end
end
desc 'Delete beneficiary'
params do
requires :id,
type: { value: Integer, message: 'account.beneficiary.non_integer_id' },
desc: 'Beneficiary Identifier in Database'
end
delete ':id' do
user_authorize! :destroy, ::Beneficiary
beneficiary = current_user
.beneficiaries
.available_to_member
.find_by!(id: params[:id])
if beneficiary.archive!
body false
else
error!({ errors: ['account.beneficiary.cant_delete'] }, 422)
end
end
end
end
end
end
end

View File

@@ -0,0 +1,30 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Account
class Bonuses < Grape::API
desc 'Get bonus simple data of a user',
is_array: true
get '/bonus' do
user_bonuses = Bonus.where(bonus_member: current_user)
all = user_bonuses.inject(0){ |sum, x| sum + x.amount }
h24 = user_bonuses.h24.inject(0){ |sum, x| sum + x.amount }
number = Member.where(referral_uid: current_user.uid).count
{ 'h24': h24, 'all': all, 'number': number }
end
desc 'Get bonus report',
is_array: true
get '/bonus/report' do
bonus = Bonus.where(bonus_member: current_user)
present paginate(bonus), with: Entities::Bonus
end
end
end
end
end

View File

@@ -0,0 +1,207 @@
# encoding: UTF-8
# frozen_string_literal: true
require_relative '../validations'
module API
module V2
module Account
class Deposits < Grape::API
before { deposits_must_be_permitted! }
desc 'Get your deposits history.',
is_array: true,
success: API::V2::Entities::Deposit
params do
optional :currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist' },
desc: 'Currency code'
optional :state,
values: { value: ->(v) { (Array.wrap(v) - ::Deposit.aasm.states.map(&:name).map(&:to_s)).blank? }, message: 'account.deposit.invalid_state' },
desc: 'Filter deposits by states.'
optional :txid,
type: String,
allow_blank: false,
desc: 'Deposit transaction id.'
optional :time_from,
allow_blank: { value: false, message: 'account.deposit.empty_time_from' },
type: { value: Integer, message: 'account.deposit.non_integer_time_from' },
desc: 'An integer represents the seconds elapsed since Unix epoch.'
optional :time_to,
type: { value: Integer, message: 'account.deposit.non_integer_time_to' },
allow_blank: { value: false, message: 'account.deposit.empty_time_to' },
desc: 'An integer represents the seconds elapsed since Unix epoch.'
optional :limit,
type: { value: Integer, message: 'account.deposit.non_integer_limit' },
values: { value: 1..100, message: 'account.deposit.invalid_limit' },
default: 100,
desc: "Number of deposits per page (defaults to 100, maximum is 100)."
optional :page,
type: { value: Integer, message: 'account.deposit.non_integer_page' },
values: { value: -> (p){ p.try(:positive?) }, message: 'account.deposit.non_positive_page'},
default: 1,
desc: 'Page number (defaults to 1).'
end
get "/deposits" do
user_authorize! :read, ::Deposit
currency = Currency.find(params[:currency]) if params[:currency].present?
current_user.deposits.order(id: :desc)
.tap { |q| q.where!(currency: currency) if currency }
.tap { |q| q.where!(txid: params[:txid]) if params[:txid] }
.tap { |q| q.where!(aasm_state: params[:state]) if params[:state] }
.tap { |q| q.where!('updated_at >= ?', Time.at(params[:time_from])) if params[:time_from].present? }
.tap { |q| q.where!('updated_at <= ?', Time.at(params[:time_to])) if params[:time_to].present? }
.tap { |q| present paginate(q), with: API::V2::Entities::Deposit }
end
desc 'Get details of specific deposit.' do
success API::V2::Entities::Deposit
end
params do
requires :txid,
type: String,
allow_blank: false,
desc: "Deposit transaction id"
end
get "/deposits/:txid" do
user_authorize! :read, ::Deposit
deposit = current_user.deposits.find_by!(txid: params[:txid])
present deposit, with: API::V2::Entities::Deposit
end
desc 'Returns deposit address for account you want to deposit to by currency. ' \
'The address may be blank because address generation process is still in progress. ' \
'If this case you should try again later.',
success: API::V2::Entities::Deposit
params do
requires :currency,
type: String,
values: { value: -> { Currency.coins.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist'},
desc: 'The account you want to deposit to.'
given :currency do
optional :address_format,
type: String,
values: { value: -> { %w[legacy cash] }, message: 'account.deposit_address.invalid_address_format' },
validate_currency_address_format: { value: true, prefix: 'account.deposit_address' },
desc: 'Address format legacy/cash'
end
end
get '/deposit_address/:currency', requirements: { currency: /[\w\.\-]+/ } do
user_authorize! :read, ::PaymentAddress
currency = Currency.find(params[:currency])
unless currency.deposit_enabled?
error!({ errors: ['account.currency.deposit_disabled'] }, 422)
end
wallet = Wallet.deposit_wallet(currency.id)
unless wallet.present?
error!({ errors: ['account.wallet.not_found'] }, 422)
end
payment_address = current_user.payment_address(wallet.id)
present payment_address, with: API::V2::Entities::PaymentAddress, address_format: params[:address_format]
end
############
desc 'Create fiat deposits'
params do
requires :amount,
type: { value: BigDecimal, message: 'deposit.fiat.non_decimal_amount' },
values: { value: ->(v) { v.try(:positive?) }, message: 'deposit.fiat.non_positive_amount' },
desc: 'The amount to deposit fiat.'
requires :callback_url,
regexp: { value: URI::regexp, message: 'callback_url.invalid' },
desc: -> { 'callback_url payment url' }
requires :currency,
type: String,
default: 'irt',
desc: -> { 'currency' }
requires :card,
type: String,
desc: 'card bank number'
optional :factorNumber,
type: String,
allow_blank: true,
desc: 'factor number'
optional :description,
type: String,
allow_blank: true,
desc: 'description'
end
post '/deposits/fiat' do
user_authorize! :create, ::Deposit
error!({ errors: ['account.deposit.use_valid_card'] }, 403) if current_user.cards.exclude?(params[:card].to_s)
deposit_data = { member: current_user, currency: Currency.find(params['currency']), aasm_state: 'canceled',
address: params['card'], amount: params['amount'], transfer_type: :fiat }
deposit = ::Deposits::Fiat.new(deposit_data)
error!({ errors: deposit.errors.full_messages }, 422) if deposit.errors.any?
data = { amount: (params[:amount] * 10), callback_url: params[:callback_url],
description: params[:description], valid_card_number: params[:card] }
response = VandarService::generate_token(data)
if response.dig('status').to_i.positive?
deposit.tap { |d| d.tid = response.dig('token') }.save!
present(response: response.dig('token'))
else
present(response: response)
status 422
end
end
desc 'Confirm fiat deposits'
params do
requires :token,
type: String,
allow_blank: false,
desc: 'payment token'
end
post '/deposits/confirm' do
deposit = ::Deposits::Fiat.find_by(tid: params[:token])
error!({ errors: ['corresponding.deposit.record.not.found'] }, 403) unless deposit.present?
# because vandar just response one
if deposit.txid.present?
db_response = {
status: 1,
amount: deposit.amount,
wage: deposit.fee,
cardNumber: deposit.address,
transId: deposit.txid
}
return present(response: db_response)
end
# first step: check everything is ok
response = ::VandarService.transaction(params[:token])
if response.dig('status').to_i.positive? && response.dig('trackingCode').present?
if response['cardNumber'].last(4).to_i == deposit.address.last(4).to_i
response = VandarService.verify(params[:token])
if response.dig('status').to_i.positive?
deposit.update(txid: response['transId'], fee: response['wage'].to_f / 10, aasm_state: 'submitted')
deposit.charge!
end
end
# TODO we should see the response and check it for rial and toman confirmation
present(response: response)
else
present(response: response)
status 422
end
# second step: verify payment
end
end
end
end
end

View File

@@ -0,0 +1,90 @@
# frozen_string_literal: true
module API
module V2
module Account
class InternalTransfers < Grape::API
namespace :internal_transfers do
desc 'List your internal transfers as paginated collection.',
is_array: true,
success: API::V2::Entities::InternalTransfer
params do
optional :currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist' },
desc: 'Currency code.'
optional :state, type: String, desc: 'The state to filter by.'
optional :sender
end
get do
user_authorize! :read, ::InternalTransfer
ransack_params = ::API::V2::Admin::Helpers::RansackBuilder.new(params)
.eq(:state)
.translate(currency: :currency_id)
.merge(g: [
{ sender_id_eq: current_user.id, receiver_id_eq: current_user.id, m: 'or' }
]).build
search = InternalTransfer.ransack(ransack_params)
.result
.order('id desc')
present paginate(search), with: API::V2::Entities::InternalTransfer, current_user: current_user
end
desc 'Creates internal transfer.'
params do
requires :currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist' },
desc: 'The currency code.'
requires :amount,
type: { value: BigDecimal, message: 'account.internal_transfer.non_decimal_amount' },
values: { value: ->(v) { v.try(:positive?) }, message: 'account.internal_transfer.non_positive_amount' },
desc: 'The amount to transfer.'
requires :otp,
type: { value: Integer, message: 'account.internal_transfer.non_integer_otp' },
allow_blank: false,
desc: 'OTP to perform action'
requires :username_or_uid,
type: String,
allow_blank: false,
desc: 'Receiver uid or username.'
end
post do
receiver = Member.find_by_username_or_uid(params[:username_or_uid])
error!({ errors: ['account.internal_transfer.receiver_not_found'] }, 422) if receiver.nil?
currency = Currency.find(params[:currency])
unless Vault::TOTP.validate?(current_user.uid, params[:otp])
error!({ errors: ['account.internal_transfer.invalid_otp'] }, 422)
end
if current_user.get_account(currency).balance < params[:amount]
error!({ errors: ['account.internal_transfer.insufficient_balance'] }, 422)
end
if current_user == receiver
error!({ errors: ['account.internal_transfer.can_not_tranfer_to_yourself'] }, 422)
end
internal_transfer = ::InternalTransfer.new(
currency: currency,
sender: current_user,
receiver: receiver,
amount: params[:amount]
)
if internal_transfer.save
present internal_transfer, with: API::V2::Entities::InternalTransfer
status 201
else
body errors: internal_transfer.errors.full_messages
status 422
end
end
end
end
end
end
end

View File

@@ -0,0 +1,28 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Account
class Levels < Grape::API
desc 'Get current level and next level informations',
is_array: true
get '/levels' do
current_level = current_user.group
next_level = current_user.next_group
current_level_min_max = Member.min_max_group(current_level)
next_level_min_max = Member.min_max_group(next_level)
current_level_fee = TradingFee.for(group: current_level, market_id: 'ANY')
next_level_fee = TradingFee.for(group: next_level, market_id: 'ANY')
{ 'current': { 'name': current_level, 'max': current_level_min_max[1], 'min': current_level_min_max[0],
'maker_fee': current_level_fee.maker, 'taker_fee': current_level_fee.taker },
'next': { 'name': next_level, 'max': next_level_min_max[1], 'min': next_level_min_max[0],
'maker_fee': next_level_fee.maker, 'taker_fee': next_level_fee.taker }}
end
end
end
end
end

View File

@@ -0,0 +1,21 @@
module API::V2
module Account
class Mount < Grape::API
before { authenticate! }
before { set_ets_context! }
mount Account::Balances
mount Account::Deposits
mount Account::Beneficiaries
mount Account::Withdraws
mount Account::Transactions
mount Account::Stats
mount Account::InternalTransfers
mount Account::Otp
mount Account::Treasury
mount Account::Levels
mount Account::Bonuses
end
end
end

78
app/api/v2/account/otp.rb Normal file
View File

@@ -0,0 +1,78 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Account
class Otp < Grape::API
helpers API::V2::Account::Utils
resource :otp do
# for example
# for withdraw with id 3
# we create a cache with name -> withdraw_3_2fa
#
# # for example
# # for deposit with id 6
# # we create a cache with name -> deposit_6_2fa
desc 'validate user otp'
params do
requires :otp,
type: { value: Integer, message: 'integer_otp' },
allow_blank: false,
desc: 'OTP to perform action'
optional :action,
type: String,
desc: 'action for top'
# optional :id,
# type: String,
# desc: 'if for top action'
end
post do
error!({ errors: ['account.not_active_otp'] }, 422) unless current_user.otp
error!({ errors: ['account.invalid_otp'] }, 422) unless Vault::TOTP.validate?(current_user.uid, params[:otp])
action = 'validate'
# id = '0'
action = params[:action] if params[:action].present?
# id = params[:id] if params[:id].present?
current_user.write_cache("#{action}_2fa", 1, 60)
status 200
end
desc 'Send confirmations code',
success: { code: 201, message: 'Generated verification code' },
failure: [
{ code: 400, message: 'Required params are missing' },
{ code: 422, message: 'Validation errors' }
]
params do
requires :data,
type: String,
allow_blank: false,
desc: 'Account email or Telephone number'
requires :action,
type: String,
allow_blank: false,
desc: 'for what need auth code'
optional :channel,
type: String,
allow_blank: true,
default: 'email',
desc: 'channel that send in'
optional :captcha_response,
types: [String, Hash],
desc: 'Response from captcha widget'
end
post '/resend' do
current_user = Member.find_by(email: params[:data])
return status 201 if current_user.nil?
publish_confirmation_code(current_user, params[:action])
status 201
end
end
end
end
end
end

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
module API
module V2
module Account
class Stats < Grape::API
desc 'Get assets pnl calculated into one currency'
params do
optional :pnl_currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'pnl.currency.doesnt_exist' },
desc: 'Currency code in which the PnL is calculated'
end
get '/stats/pnl' do
user_authorize! :read, ::StatsMemberPnl
query = 'SELECT pnl_currency_id, currency_id, total_credit, total_debit, total_credit_value, total_debit_value, ' \
'total_credit_value / NULLIF(total_credit, 0) "average_buy_price", ' \
'total_debit_value / NULLIF(total_debit, 0) "average_sell_price", ' \
'average_balance_price, total_balance_value ' \
'FROM stats_member_pnl WHERE member_id = ?'
conditions = [current_user.id]
if params[:pnl_currency].present?
query += ' AND pnl_currency_id = ?'
conditions << params[:pnl_currency]
end
squery = ActiveRecord::Base.sanitize_sql_for_conditions([query] + conditions)
result = ActiveRecord::Base.connection.exec_query(squery).to_hash
present result.each(&:symbolize_keys!), with: API::V2::Entities::Pnl
end
end
end
end
end

View File

@@ -0,0 +1,109 @@
# encoding: UTF-8
# frozen_string_literal: true
require_relative '../validations'
module API
module V2
module Account
class Transactions < Grape::API
before { deposits_must_be_permitted! }
before { withdraws_must_be_permitted! }
desc 'Get your transactions history.',
is_array: true
params do
optional :currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.transactions.currency_doesnt_exist' },
desc: 'Currency code'
optional :order_by,
type: String,
values: { value: %w(asc desc), message: 'account.transactions.order_by_invalid' },
default: 'desc',
desc: 'Sorting order'
optional :time_from,
allow_blank: { value: false, message: 'account.transactions.empty_time_from' },
type: { value: Integer, message: 'account.transactions.non_integer_time_from' },
desc: 'An integer represents the seconds elapsed since Unix epoch.'
optional :time_to,
type: { value: Integer, message: 'account.transactions.non_integer_time_to' },
allow_blank: { value: false, message: 'account.transactions.empty_time_to' },
desc: 'An integer represents the seconds elapsed since Unix epoch.'
optional :deposit_state,
values: { value: ->(v) { (Array.wrap(v) - ::Deposit.aasm.states.map(&:name).map(&:to_s)).blank? }, message: 'account.transactions.invalid_deposit_state' },
desc: 'Filter deposits by states.',
default: []
optional :withdraw_state,
values: { value: ->(v) { (Array.wrap(v) - Withdraw::STATES.map(&:to_s)).blank? }, message: 'account.transactions.invalid_withdraw_state' },
desc: 'Filter withdraws by states.',
default: []
optional :txid,
type: String,
allow_blank: false,
desc: 'Transaction id.'
optional :limit,
type: { value: Integer, message: 'account.transactions.non_integer_limit' },
values: { value: 1..1000, message: 'account.transactions.invalid_limit' },
default: 100,
desc: 'Limit the number of returned transactions. Default to 100.'
optional :page,
type: { value: Integer, message: 'account.transactions.non_integer_page' },
values: { value: -> (p){ p.try(:positive?) }, message: 'account.transactions.non_positive_page'},
allow_blank: false,
default: 1,
desc: 'Specify the page of paginated results.'
end
get "/transactions" do
user_authorize! :read, ::Withdraw
user_authorize! :read, ::Deposit
deposit_state = params[:deposit_state]&.split(/\W+/)&.join(',')
withdraw_state = params[:withdraw_state]&.split(/\W+/)&.join(',')
deposit_sql = "(SELECT d.id, currency_id, amount, fee, address, aasm_state, NULL AS note, txid, d.created_at, d.updated_at, d.type, b.height - block_number AS confirmations FROM deposits d " \
"INNER JOIN currencies c ON c.id=d.currency_id LEFT JOIN blockchains b ON b.key=c.blockchain_key WHERE member_id=#{current_user.id} "
if params[:deposit_state].present?
deposit_sql += if Rails.configuration.database_adapter.downcase == 'PostgreSQL'.downcase
"and aasm_state = any(string_to_array('#{deposit_state}',','))"
else
"and FIND_IN_SET(aasm_state, '#{deposit_state}')"
end
end
withdraw_sql = "SELECT w.id, currency_id, amount, fee, rid, aasm_state, note, txid, w.created_at, w.updated_at, w.type, b.height - block_number AS confirmations FROM withdraws w " \
"INNER JOIN currencies c ON c.id=w.currency_id LEFT JOIN blockchains b ON b.key=c.blockchain_key WHERE member_id=#{current_user.id} "
if params[:withdraw_state].present?
withdraw_sql += if Rails.configuration.database_adapter.downcase == 'PostgreSQL'.downcase
"and aasm_state = any(string_to_array('#{withdraw_state}',','))"
else
"and FIND_IN_SET(aasm_state, '#{withdraw_state}')"
end
end
sql = "SELECT * FROM " + deposit_sql + "UNION " + withdraw_sql + ") AS transactions ORDER BY updated_at #{params[:order_by].upcase}"
result = ActiveRecord::Base.connection.exec_query(sql).to_hash
result.select! { |t| t['currency_id'] == params[:currency].downcase } if params[:currency].present?
result.select! { |t| t['txid'] == params[:txid] } if params[:txid].present?
result.select! { |t| t['updated_at'] >= Time.at(params[:time_from]) } if params[:time_from].present?
result.select! { |t| t['updated_at'] <= Time.at(params[:time_to]) } if params[:time_to].present?
present paginate(result.each(&:symbolize_keys!)), with: API::V2::Entities::Transactions
end
end
end
end
end

View File

@@ -0,0 +1,43 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Account
class Treasury < Grape::API
helpers ::API::V2::ParamHelpers
desc 'Get list of user cards',
is_array: true
params do
# use :pagination
optional :uid,
type: String,
default: false,
desc: 'UID of user'
end
get '/treasury/cards' do
user_authorize! :read, ::Operations::Account
present current_user.cards
end
desc 'Get list of user ibans',
is_array: true
params do
# use :pagination
optional :uid,
type: String,
default: false,
desc: 'UID of user'
end
get '/treasury/ibans' do
user_authorize! :read, ::Operations::Account
present current_user.ibans
end
end
end
end
end

View File

@@ -0,0 +1,19 @@
module API
module V2
module Account
module Utils
def session
request.session
end
def publish_confirmation_code(user, action)
totp = ::Vault::TOTPAction.new(action)
totp.create(user.uid, user.email)
::EventAPI.notify(action, record: { user: user.as_json_for_event_api,
domain: Peatio::App.config.domain,
code: totp.read_code(user.uid) })
end
end
end
end
end

View File

@@ -0,0 +1,245 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Account
class Withdraws < Grape::API
before { withdraws_must_be_permitted! }
helpers API::V2::Account::Utils
desc 'List your withdraws as paginated collection.',
is_array: true,
success: API::V2::Entities::Withdraw
params do
optional :currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist'},
desc: 'Currency code.'
optional :limit,
type: { value: Integer, message: 'account.withdraw.non_integer_limit' },
values: { value: 1..100, message: 'account.withdraw.invalid_limit' },
default: 100,
desc: "Number of withdraws per page (defaults to 100, maximum is 100)."
optional :state,
values: { value: ->(v) { (Array.wrap(v) - Withdraw::STATES.map(&:to_s)).blank? }, message: 'account.withdraw.invalid_state' },
desc: 'Filter withdrawals by states.'
optional :rid,
type: String,
allow_blank: false,
desc: 'Wallet address on the Blockchain.'
optional :time_from,
allow_blank: { value: false, message: 'account.withdraw.empty_time_from' },
type: { value: Integer, message: 'account.withdraw.non_integer_time_from' },
desc: 'An integer represents the seconds elapsed since Unix epoch.'
optional :time_to,
type: { value: Integer, message: 'account.withdraw.non_integer_time_to' },
allow_blank: { value: false, message: 'account.withdraw.empty_time_to' },
desc: 'An integer represents the seconds elapsed since Unix epoch.'
optional :page,
type: { value: Integer, message: 'account.withdraw.non_integer_page' },
values: { value: -> (p){ p.try(:positive?) }, message: 'account.withdraw.non_positive_page'},
default: 1,
desc: 'Page number (defaults to 1).'
end
get '/withdraws' do
user_authorize! :read, ::Withdraw
currency = Currency.find(params[:currency]) if params[:currency].present?
current_user.withdraws.order(id: :desc)
.tap { |q| q.where!(currency: currency) if currency }
.tap { |q| q.where!(aasm_state: params[:state]) if params[:state] }
.tap { |q| q.where!(rid: params[:rid]) if params[:rid] }
.tap { |q| q.where!('updated_at >= ?', Time.at(params[:time_from])) if params[:time_from].present? }
.tap { |q| q.where!('updated_at <= ?', Time.at(params[:time_to])) if params[:time_to].present? }
.tap { |q| present paginate(q), with: API::V2::Entities::Withdraw }
end
desc 'Returns withdrawal sums for last 4 hours and 1 month'
get '/withdraws/sums' do
user_authorize! :read, ::Withdraw
sum_24_hours, sum_1_month = Withdraw.sanitize_execute_sum_queries(current_user.id)
present({ last_24_hours: sum_24_hours, last_1_month: sum_1_month })
end
desc 'Creates new withdrawal to active beneficiary.'
params do
requires :otp,
type: { value: Integer, message: 'account.withdraw.non_integer_otp' },
allow_blank: false,
desc: 'OTP to perform action'
requires :beneficiary_id,
type: { value: Integer, message: 'account.withdraw.non_integer_beneficiary_id' },
allow_blank: false,
desc: 'ID of Active Beneficiary belonging to user.'
requires :currency,
type: String,
values: { value: -> { Currency.visible.codes(bothcase: true) }, message: 'account.currency.doesnt_exist'},
desc: 'The currency code.'
requires :amount,
type: { value: BigDecimal, message: 'account.withdraw.non_decimal_amount' },
values: { value: ->(v) { v.try(:positive?) }, message: 'account.withdraw.non_positive_amount' },
desc: 'The amount to withdraw.'
optional :note,
type: String,
values: { value: ->(v) { v.size <= 256 }, message: 'account.withdraw.too_long_note' },
desc: 'Optional user metadata to be applied to the transaction. Used to tag transactions with memorable comments.'
end
post '/withdraws' do
user_authorize! :create, ::Withdraw
withdraw_api_must_be_enabled!
if current_user.otp.present? && current_user.read_cache('withdraw_2fa').blank?
error!({ errors: ['account.withdraw.need.2fa'] }, 403)
end
totp = Vault::TOTPAction.new('withdraw-coin')
error!({ errors: ['withdraw.totp.code'] }, 422) unless totp.validate?(current_user.uid, params[:otp])
beneficiary = current_user
.beneficiaries
.available_to_member
.find_by(id: params[:beneficiary_id])
if beneficiary.blank?
error!({ errors: ['account.beneficiary.doesnt_exist'] }, 422)
elsif !beneficiary.active?
error!({ errors: ['account.beneficiary.invalid_state_for_withdrawal'] }, 422)
end
currency = Currency.find(params[:currency])
error!({ errors: ['account.currency.withdrawal_disabled'] }, 422) unless currency.withdrawal_enabled?
# TODO: Delete subclasses from Deposit and Withdraw
withdraw = "withdraws/#{currency.type}".camelize.constantize.new \
beneficiary: beneficiary,
sum: params[:amount],
member: current_user,
currency: currency,
note: params[:note]
withdraw.save!
withdraw.with_lock { withdraw.accept! }
current_user.delete_cache('withdraw_2fa')
present withdraw, with: API::V2::Entities::Withdraw
rescue ::Account::AccountError => e
report_api_error(e, request)
error!({ errors: ['account.withdraw.insufficient_balance'] }, 422)
rescue ActiveRecord::RecordInvalid => e
report_api_error(e, request)
# TODO: Check if there are other errors possible here.
# For now single error which is not handled by params validations is
# sum precision validation error (PrecisionValidator).
error!({ errors: ['account.withdraw.invalid_amount'] }, 422)
rescue => e
report_exception(e)
error!({ errors: ['account.withdraw.create_error'] }, 422)
end
desc 'Creates new fiat withdrawal'
params do
requires :amount,
type: { value: BigDecimal, message: 'account.withdraw.non_decimal_amount' },
values: { value: ->(v) { v.try(:positive?) }, message: 'account.withdraw.non_positive_amount' },
desc: 'The amount to withdraw.'
requires :iban,
type: String,
desc: 'iban bank number'
requires :currency,
type: String,
default: 'irt',
desc: -> { 'currency' }
optional :note,
type: String,
values: { value: ->(v) { v.size <= 256 }, message: 'account.withdraw.too_long_note' },
desc: 'Optional user metadata to be applied to the transaction. Used to tag transactions with memorable comments.'
end
post '/withdraws/fiat' do
user_authorize! :create, ::Withdraw
withdraw_api_must_be_enabled!
if current_user.otp.present? && current_user.read_cache('withdraw_2fa').blank?
error!({ errors: ['account.withdraw.need.2fa'] }, 403)
end
error!({ errors: ['account.withdraw.use_valid_iban'] }, 403) if current_user.ibans.exclude?(params[:iban])
currency = Currency.find(params[:currency])
withdraw = "withdraws/#{currency.type}".camelize.constantize.new \
rid: params[:iban],
sum: params[:amount],
member: current_user,
currency: currency,
transfer_type: :fiat,
note: params[:note]
withdraw.save
error!({ errors: withdraw.errors.full_messages }, 422) unless withdraw.valid?
# delete 2fa cache
current_user.delete_cache('withdraw_2fa')
publish_confirmation_code(current_user, 'withdraw-fiat')
present withdraw, with: API::V2::Entities::Withdraw
end
#
#
desc 'confirm fiat withdrawal'
params do
requires :id,
type: Integer,
desc: 'withdraw id'
requires :otp,
type: Integer,
desc: 'auth otp in sms or email'
end
post '/withdraws/confirm' do
user_authorize! :create, ::Withdraw
withdraw = current_user.withdraws.find_by(id: params[:id])
error!({ errors: ['account.withdraw.not.found'] }, 403) unless withdraw.present?
error!({ errors: ['account.withdraw.process_before'] }, 403) unless withdraw.aasm_state == 'prepared'
totp = Vault::TOTPAction.new('withdraw-fiat')
error!({ errors: ['withdraw.totp.code'] }, 422) unless totp.validate?(current_user.uid, params[:otp])
# here must lock found
withdraw.with_lock { withdraw.accept! }
response = ::VandarService.new.create_withdraw(amount: withdraw.sum, iban: withdraw.rid)
if response.dig('status').to_i.positive?
withdraw.update(txid: response['data'].dig('settlement', 0, 'transaction_id'), aasm_state: 'confirming')
else
withdraw.with_lock { withdraw.reject! }
end
present(response: response)
end
#
#
desc 'Get fiat withdrawal'
params do
requires :id,
type: Integer,
desc: 'withdraw id'
optional :with_otp,
type: String,
desc: -> { 'send otp or not' }
end
get '/withdraw' do
user_authorize! :read, ::Withdraw
withdraw = current_user.withdraws.find_by(id: params[:id])
if withdraw.present? && withdraw.accepted? && params['with_otp'].present?
publish_confirmation_code(current_user, 'withdraw')
end
present withdraw, with: API::V2::Entities::Withdraw
end
end
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

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

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

View File

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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