Skip to content

Commit

Permalink
Implement skimming deposit collection mechanism based on wallet max_b…
Browse files Browse the repository at this point in the history
…alance (closes #1653) (#1735)

* Added Wallet Service Specs

* Implement skimming deposit collection mechanism for bitcoin

* Implement skimming deposit collection mechanism for eth

* Implement skimming deposit collection mechanism for ripple

* Enumerize Wallet kind

* Edit skiming mechanism for ripple wallet

* Edit skiming mechanism for eth wallet

* Use hash instead array for deposit collections

* Update specs with new skiming mechanism and add specs for ripple

* Add min_collection_amount method to currency

* Move load_balance! request to blockchain service and add specs for wallet service

* Add min_collection_amount column to currency and move spread deposit method to protected
  • Loading branch information
Maksym authored and Louis committed Nov 26, 2018
1 parent c648f93 commit e3d83a6
Show file tree
Hide file tree
Showing 39 changed files with 1,917 additions and 205 deletions.
1 change: 1 addition & 0 deletions app/controllers/admin/currencies_controller.rb
Expand Up @@ -54,6 +54,7 @@ def permitted_currency_attributes
icon_url
quick_withdraw_limit
min_deposit_amount
min_collection_amount
withdraw_fee
deposit_fee
enabled
Expand Down
34 changes: 18 additions & 16 deletions app/models/currency.rb
Expand Up @@ -22,6 +22,7 @@ class Currency < ActiveRecord::Base

validates :quick_withdraw_limit,
:min_deposit_amount,
:min_collection_amount,
:withdraw_fee,
:deposit_fee,
numericality: { greater_than_or_equal_to: 0 }
Expand Down Expand Up @@ -147,25 +148,26 @@ def must_not_disable_all_markets
end

# == Schema Information
# Schema version: 20181004114428
# Schema version: 20181126101312
#
# Table name: currencies
#
# id :string(10) not null, primary key
# blockchain_key :string(32)
# symbol :string(1) not null
# type :string(30) default("coin"), not null
# deposit_fee :decimal(32, 16) default(0.0), not null
# quick_withdraw_limit :decimal(32, 16) default(0.0), not null
# min_deposit_amount :decimal(32, 16) default(0.0), not null
# withdraw_fee :decimal(32, 16) default(0.0), not null
# options :string(1000) default({}), not null
# enabled :boolean default(TRUE), not null
# base_factor :integer default(1), not null
# precision :integer default(8), not null
# icon_url :string(255)
# created_at :datetime not null
# updated_at :datetime not null
# id :string(10) not null, primary key
# blockchain_key :string(32)
# symbol :string(1) not null
# type :string(30) default("coin"), not null
# deposit_fee :decimal(32, 16) default(0.0), not null
# quick_withdraw_limit :decimal(32, 16) default(0.0), not null
# min_deposit_amount :decimal(32, 16) default(0.0), not null
# min_collection_amount :decimal(32, 16) default(0.0), not null
# withdraw_fee :decimal(32, 16) default(0.0), not null
# options :string(1000) default({}), not null
# enabled :boolean default(TRUE), not null
# base_factor :integer default(1), not null
# precision :integer default(8), not null
# icon_url :string(255)
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
Expand Down
57 changes: 51 additions & 6 deletions app/models/wallet.rb
@@ -1,8 +1,17 @@
# encoding: UTF-8
# frozen_string_literal: true


class Wallet < ActiveRecord::Base
KIND = %w[hot warm cold deposit fee].freeze
extend Enumerize

# We use this attribute values rules for wallet kinds:
# 1** - for deposit wallets.
# 2** - for fee wallets.
# 3** - for withdraw wallets (sorted by security hot < warm < cold).
ENUMERIZED_KINDS = { deposit: 100, fee: 200, hot: 310, warm: 320, cold: 330 }.freeze
enumerize :kind, in: ENUMERIZED_KINDS, scope: true

GATEWAYS = %w[bitcoind bitcoincashd litecoind geth dashd rippled bitgo].freeze
SETTING_ATTRIBUTES = %i[ uri
secret
Expand All @@ -22,29 +31,58 @@ class Wallet < ActiveRecord::Base
validates :address, presence: true

validates :status, inclusion: { in: %w[active disabled] }
validates :kind, inclusion: { in: KIND }
validates :gateway, inclusion: { in: GATEWAYS }

validates :nsig, numericality: { greater_than_or_equal_to: 1, only_integer: true }
validates :max_balance, numericality: { greater_than_or_equal_to: 0 }
validates :uri, url: { allow_blank: true }

scope :active, -> { where(status: :active) }
scope :deposit, -> { where(kind: :deposit) }
scope :withdraw, -> { where.not(kind: :deposit) }
scope :deposit, -> { where(kind: kinds(deposit: true, values: true)) }
scope :fee, -> { where(kind: kinds(fee: true, values: true)) }
scope :withdraw, -> { where(kind: kinds(withdraw: true, values: true)) }
scope :ordered, -> { order(kind: :asc) }

before_validation do
next unless blockchain_api&.supports_cash_addr_format? && address?
self.address = CashAddr::Converter.to_cash_address(address)
end

class << self
def kinds(options={})
ENUMERIZED_KINDS
.yield_self do |kinds|
case
when options.fetch(:deposit, false)
kinds.select { |_k, v| v / 100 == 1 }
when options.fetch(:fee, false)
kinds.select { |_k, v| v / 100 == 2 }
when options.fetch(:withdraw, false)
kinds.select { |_k, v| v / 100 == 3 }
else
kinds
end
end
.yield_self do |kinds|
case
when options.fetch(:keys, false)
kinds.keys
when options.fetch(:values, false)
kinds.values
else
kinds
end
end
end
end

def wallet_url
blockchain.explorer_address.gsub('#{address}', address) if blockchain
end
end

# == Schema Information
# Schema version: 20180813105100
# Schema version: 20181017114624
#
# Table name: wallets
#
Expand All @@ -53,7 +91,7 @@ def wallet_url
# currency_id :string(10)
# name :string(64)
# address :string(255) not null
# kind :string(32) not null
# kind :integer not null
# nsig :integer
# gateway :string(20) default(""), not null
# settings :string(1000) default({}), not null
Expand All @@ -63,3 +101,10 @@ def wallet_url
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_wallets_on_currency_id (currency_id)
# index_wallets_on_kind (kind)
# index_wallets_on_kind_and_currency_id_and_status (kind,currency_id,status)
# index_wallets_on_status (status)
#
43 changes: 39 additions & 4 deletions app/services/wallet_service.rb
Expand Up @@ -46,13 +46,48 @@ def create_address!

protected

def destination_wallet(deposit)
# TODO: Dynamicly check wallet balance and select destination wallet.
# For keeping it simple we will collect all deposits to hot wallet.
def spread_deposit(deposit)
left_amount = deposit.amount
collection_spread = Hash.new(0)
currency = deposit.currency
destination_wallets(deposit).each do |wallet|
break if left_amount == 0
blockchain_client = BlockchainClient[Blockchain.find_by_key(wallet.blockchain_key).key]
wallet_balance = blockchain_client.load_balance!(wallet.address, deposit.currency)
amount_for_wallet = [wallet.max_balance - wallet_balance, left_amount].min
# If free amount for current wallet too small we will not able to collect it.
# So we try to collect it to next wallets.
next if amount_for_wallet < currency.min_collection_amount
left_amount -= amount_for_wallet
# If amount left is too small we will not able to collect it.
# So we collect everything to current wallet.
if left_amount < currency.min_collection_amount
amount_for_wallet += left_amount
left_amount = 0
end
collection_spread[wallet.address] = amount_for_wallet if amount_for_wallet > 0
rescue => e
# If have exception move to next wallet
report_exception(e)
end
# If deposit doesn't fit to any wallet collect it to last wallet.
# Last wallet is considered to be the most secure.
if left_amount > 0
collection_spread[destination_wallets(deposit).last.address] += left_amount
left_amount = 0
end
unless collection_spread.values.sum == deposit.amount
raise Error, "Deposit spread failed deposit.amount != collection_spread.values.sum"
end
collection_spread
end

def destination_wallets(deposit)
Wallet
.active
.withdraw
.find_by(currency_id: deposit.currency_id, kind: :hot)
.ordered
.where(currency_id: deposit.currency_id)
end
end
end
19 changes: 10 additions & 9 deletions app/services/wallet_service/bitcoind.rb
Expand Up @@ -9,18 +9,19 @@ def create_address(options = {})
end

def collect_deposit!(deposit, options={})
destination_address = destination_wallet(deposit).address
pa = deposit.account.payment_address

# this will automatically deduct fee from amount
# This will automatically deduct fee from amount so we can withdraw exact amount.
options = options.merge( subtract_fee: true )

client.create_withdrawal!(
{ address: pa.address },
{ address: destination_address },
deposit.amount,
options
)
spread_hash = spread_deposit(deposit)
spread_hash.map do |address, amount|
client.create_withdrawal!(
{ address: pa.address },
{ address: address},
amount,
options
)
end
end

def build_withdrawal!(withdraw, options = {})
Expand Down
51 changes: 27 additions & 24 deletions app/services/wallet_service/geth.rb
Expand Up @@ -13,11 +13,11 @@ def create_address(options = {})
end

def collect_deposit!(deposit, options={})
destination_address = destination_wallet(deposit).address
destination_wallets = destination_wallets(deposit)
if deposit.currency.code.eth?
collect_eth_deposit!(deposit, destination_address, options)
collect_eth_deposit!(deposit, destination_wallets, options)
else
collect_erc20_deposit!(deposit, destination_address, options)
collect_erc20_deposit!(deposit, destination_wallets, options)
end
end

Expand All @@ -30,12 +30,12 @@ def build_withdrawal!(withdraw)
end

def deposit_collection_fees(deposit, value=DEFAULT_ERC20_FEE_VALUE, options={})
fees_wallet = erc20_fee_wallet
fee_wallet = erc20_fee_wallet
destination_address = deposit.account.payment_address.address
options = DEFAULT_ETH_FEE.merge options

client.create_eth_withdrawal!(
{ address: fees_wallet.address, secret: fees_wallet.secret },
{ address: fee_wallet.address, secret: fee_wallet.secret },
{ address: destination_address },
value,
options
Expand All @@ -47,35 +47,38 @@ def deposit_collection_fees(deposit, value=DEFAULT_ERC20_FEE_VALUE, options={})
def erc20_fee_wallet
Wallet
.active
.withdraw
.find_by(currency_id: :eth, kind: :fee)
end

def collect_eth_deposit!(deposit, destination_address, options={})
def collect_eth_deposit!(deposit, destination_wallets, options={})
# Default values for Ethereum tx fees.
options = DEFAULT_ETH_FEE.merge options

# We can't collect all funds we need to subtract gas fees.
amount = deposit.amount_to_base_unit! - options[:gas_limit] * options[:gas_price]
pa = deposit.account.payment_address
client.create_eth_withdrawal!(
{ address: pa.address, secret: pa.secret },
{ address: destination_address },
amount,
options
)
spread_hash = spread_deposit(deposit)
spread_hash.map do |address, amount|
spread_amount = amount * deposit.currency.base_factor - options[:gas_limit] * options[:gas_price]
client.create_eth_withdrawal!(
{ address: pa.address, secret: pa.secret },
{ address: address},
spread_amount.to_i,
options
)
end
end

def collect_erc20_deposit!(deposit, destination_address, options={})
def collect_erc20_deposit!(deposit, destination_wallets, options={})
pa = deposit.account.payment_address

client.create_erc20_withdrawal!(
{ address: pa.address, secret: pa.secret },
{ address: destination_address },
deposit.amount_to_base_unit!,
options.merge(contract_address: deposit.currency.erc20_contract_address )
)

spread_hash = spread_deposit(deposit)
spread_hash.map do |address, amount|
spread_amount = amount * deposit.currency.base_factor
client.create_erc20_withdrawal!(
{ address: pa.address, secret: pa.secret },
{ address: address},
spread_amount.to_i,
options.merge( contract_address: deposit.currency.erc20_contract_address )
)
end
end

def build_eth_withdrawal!(withdraw)
Expand Down
17 changes: 9 additions & 8 deletions app/services/wallet_service/rippled.rb
Expand Up @@ -11,15 +11,16 @@ def create_address(options = {})
end

def collect_deposit!(deposit, options={})
destination_address = destination_wallet(deposit).address
pa = deposit.account.payment_address

client.create_withdrawal!(
{ address: pa.address, secret: pa.secret },
{ address: destination_address },
deposit.amount,
options
)
spread_hash = spread_deposit(deposit)
spread_hash.map do |address, amount|
client.create_withdrawal!(
{ address: pa.address, secret: pa.secret },
{ address: address },
amount,
options
)
end
end

def build_withdrawal!(withdraw, options = {})
Expand Down
5 changes: 4 additions & 1 deletion app/views/admin/currencies/show.html.erb
Expand Up @@ -33,9 +33,12 @@
<dt>Deposit fee (fiat only)</dt>
<dd><%= f.text_field :deposit_fee, class: 'form-control' %></dd>

<dt>Min Deposit amount</dt>
<dt>Min deposit amount</dt>
<dd><%= f.text_field :min_deposit_amount, class: 'form-control' %></dd>

<dt>Min collection amount</dt>
<dd><%= f.text_field :min_collection_amount, class: 'form-control' %></dd>

<% if @currency.coin? || @currency.new_record? %>
<% if @currency.new_record? || @currency.erc20_contract_address? %>
<dt>ERC20 contract address</dt>
Expand Down
2 changes: 1 addition & 1 deletion app/views/admin/wallets/show.html.erb
Expand Up @@ -25,7 +25,7 @@
<%= f.text_field :address, class: 'form-control mb-3' %>

<label>Kind</label>
<%= f.select :kind, Wallet::KIND.map { |k| [k.capitalize, k] }, {selected: @wallet.kind}, {class: 'form-control mb-3'} %>
<%= f.select :kind, Wallet::kinds(keys: true).map { |k| [k.capitalize, k] }, {selected: @wallet.kind}, {class: 'form-control mb-3'} %>
</div>
</div>
</div>
Expand Down

0 comments on commit e3d83a6

Please sign in to comment.