diff --git a/app/controllers/admin/currencies_controller.rb b/app/controllers/admin/currencies_controller.rb index 4236880f9c..b2b4be86d8 100644 --- a/app/controllers/admin/currencies_controller.rb +++ b/app/controllers/admin/currencies_controller.rb @@ -54,6 +54,7 @@ def permitted_currency_attributes icon_url quick_withdraw_limit min_deposit_amount + min_collection_amount withdraw_fee deposit_fee enabled diff --git a/app/models/currency.rb b/app/models/currency.rb index 099748ae94..441937777e 100644 --- a/app/models/currency.rb +++ b/app/models/currency.rb @@ -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 } @@ -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 # diff --git a/app/models/wallet.rb b/app/models/wallet.rb index e5e40c8c5b..e50d3b3fb3 100644 --- a/app/models/wallet.rb +++ b/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 @@ -22,7 +31,6 @@ 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 } @@ -30,21 +38,51 @@ class Wallet < ActiveRecord::Base 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 # @@ -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 @@ -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) +# diff --git a/app/services/wallet_service.rb b/app/services/wallet_service.rb index e1d9d47230..45f918d0d2 100644 --- a/app/services/wallet_service.rb +++ b/app/services/wallet_service.rb @@ -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 diff --git a/app/services/wallet_service/bitcoind.rb b/app/services/wallet_service/bitcoind.rb index c670c811bb..0ae002381e 100644 --- a/app/services/wallet_service/bitcoind.rb +++ b/app/services/wallet_service/bitcoind.rb @@ -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 = {}) diff --git a/app/services/wallet_service/geth.rb b/app/services/wallet_service/geth.rb index 01a13701ac..4b5d1dfb9c 100644 --- a/app/services/wallet_service/geth.rb +++ b/app/services/wallet_service/geth.rb @@ -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 @@ -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 @@ -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) diff --git a/app/services/wallet_service/rippled.rb b/app/services/wallet_service/rippled.rb index 6abd608305..4972d4b7d3 100644 --- a/app/services/wallet_service/rippled.rb +++ b/app/services/wallet_service/rippled.rb @@ -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 = {}) diff --git a/app/views/admin/currencies/show.html.erb b/app/views/admin/currencies/show.html.erb index b731e362d8..4e50a06f1e 100644 --- a/app/views/admin/currencies/show.html.erb +++ b/app/views/admin/currencies/show.html.erb @@ -33,9 +33,12 @@
Deposit fee (fiat only)
<%= f.text_field :deposit_fee, class: 'form-control' %>
-
Min Deposit amount
+
Min deposit amount
<%= f.text_field :min_deposit_amount, class: 'form-control' %>
+
Min collection amount
+
<%= f.text_field :min_collection_amount, class: 'form-control' %>
+ <% if @currency.coin? || @currency.new_record? %> <% if @currency.new_record? || @currency.erc20_contract_address? %>
ERC20 contract address
diff --git a/app/views/admin/wallets/show.html.erb b/app/views/admin/wallets/show.html.erb index 38ffa378a9..8345936032 100644 --- a/app/views/admin/wallets/show.html.erb +++ b/app/views/admin/wallets/show.html.erb @@ -25,7 +25,7 @@ <%= f.text_field :address, class: 'form-control mb-3' %> - <%= 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'} %> diff --git a/config/templates/seed/currencies.yml.erb b/config/templates/seed/currencies.yml.erb index 2f3b26f469..5e2700fc76 100644 --- a/config/templates/seed/currencies.yml.erb +++ b/config/templates/seed/currencies.yml.erb @@ -1,121 +1,129 @@ <% if ENV['CURRENCIES_CONFIG'] %> <%= File.read(ENV['CURRENCIES_CONFIG']) %> <% else %> -- id: usd - symbol: '$' - type: fiat - precision: 2 - base_factor: 1 - enabled: true - quick_withdraw_limit: 1000 - min_deposit_amount: 0 - deposit_fee: 0 - withdraw_fee: 0 +- id: usd + symbol: '$' + type: fiat + precision: 2 + base_factor: 1 + enabled: true + quick_withdraw_limit: 1000 + min_deposit_amount: 0 + min_collection_amount: 0 + deposit_fee: 0 + withdraw_fee: 0 -- id: btc - blockchain_key: btc-testnet - symbol: '฿' - type: coin - precision: 8 - base_factor: 100_000_000 - enabled: true - quick_withdraw_limit: 0.1 +- id: btc + blockchain_key: btc-testnet + symbol: '฿' + type: coin + precision: 8 + base_factor: 100_000_000 + enabled: true + quick_withdraw_limit: 0.1 # Deposits with less amount are skipped during blockchain synchronization. # We advise to set value 10 times bigger than the network fee to prevent losses. - min_deposit_amount: 0.0000356 - deposit_fee: 0 - withdraw_fee: 0 - options: {} + min_deposit_amount: 0.0000356 + min_collection_amount: 0.0000356 + deposit_fee: 0 + withdraw_fee: 0 + options: {} -- id: xrp - blockchain_key: xrp-testnet - symbol: 'ꭆ' - type: coin - precision: 8 - base_factor: 1_000_000 - enabled: true - quick_withdraw_limit: 1000 +- id: xrp + blockchain_key: xrp-testnet + symbol: 'ꭆ' + type: coin + precision: 8 + base_factor: 1_000_000 + enabled: true + quick_withdraw_limit: 1000 # Deposits with less amount are skipped during blockchain synchronization. # We advise to set value 10 times bigger than the network fee to prevent losses. - min_deposit_amount: 0.00012 - deposit_fee: 0 - withdraw_fee: 0 - options: {} + min_deposit_amount: 0.00012 + min_collection_amount: 0.00012 + deposit_fee: 0 + withdraw_fee: 0 + options: {} -- id: bch - blockchain_key: bch-testnet - symbol: '฿' - type: coin - precision: 8 - base_factor: 100_000_000 - enabled: true - quick_withdraw_limit: 1 +- id: bch + blockchain_key: bch-testnet + symbol: '฿' + type: coin + precision: 8 + base_factor: 100_000_000 + enabled: true + quick_withdraw_limit: 1 # Deposits with less amount are skipped during blockchain synchronization. # We advise to set value 10 times bigger than the network fee to prevent losses. - min_deposit_amount: 0.0000748 - deposit_fee: 0 - withdraw_fee: 0 - options: {} + min_deposit_amount: 0.0000748 + min_collection_amount: 0.0000748 + deposit_fee: 0 + withdraw_fee: 0 + options: {} -- id: ltc - blockchain_key: ltc-testnet - symbol: 'Ł' - type: coin - precision: 8 - base_factor: 100_000_000 - enabled: true - quick_withdraw_limit: 5 +- id: ltc + blockchain_key: ltc-testnet + symbol: 'Ł' + type: coin + precision: 8 + base_factor: 100_000_000 + enabled: true + quick_withdraw_limit: 5 # Deposits with less amount are skipped during blockchain synchronization. # We advise to set value 10 times bigger than the network fee to prevent losses. - min_deposit_amount: 0.0004488 - deposit_fee: 0 - withdraw_fee: 0 - options: {} + min_deposit_amount: 0.0004488 + min_collection_amount: 0.0004488 + deposit_fee: 0 + withdraw_fee: 0 + options: {} -- id: dash - blockchain_key: dash-testnet - symbol: 'Đ' - type: coin - precision: 8 - base_factor: 100_000_000 - enabled: true - quick_withdraw_limit: 2 +- id: dash + blockchain_key: dash-testnet + symbol: 'Đ' + type: coin + precision: 8 + base_factor: 100_000_000 + enabled: true + quick_withdraw_limit: 2 # Deposits with less amount are skipped during blockchain synchronization. # We advise to set value 10 times bigger than the network fee to prevent losses. - min_deposit_amount: 0.0000226 - deposit_fee: 0 - withdraw_fee: 0 - options: {} + min_deposit_amount: 0.0000226 + min_collection_amount: 0.0000226 + deposit_fee: 0 + withdraw_fee: 0 + options: {} -- id: eth - blockchain_key: eth-rinkeby - symbol: 'Ξ' - type: coin - precision: 8 - base_factor: 1_000_000_000_000_000_000 - enabled: true - quick_withdraw_limit: 2 +- id: eth + blockchain_key: eth-rinkeby + symbol: 'Ξ' + type: coin + precision: 8 + base_factor: 1_000_000_000_000_000_000 + enabled: true + quick_withdraw_limit: 2 # Deposits with less amount are skipped during blockchain synchronization. # We advise to set value 10 times bigger than the network fee to prevent losses. - min_deposit_amount: 0.00021 - deposit_fee: 0 - withdraw_fee: 0 - options: {} + min_deposit_amount: 0.00021 + min_collection_amount: 0.00021 + deposit_fee: 0 + withdraw_fee: 0 + options: {} -- id: trst - blockchain_key: eth-rinkeby - symbol: 'Ξ' - type: coin - precision: 8 - base_factor: 1_000_000 # IMPORTANT: Don't forget to update this variable according - enabled: true # to your ERC20-based currency requirements - quick_withdraw_limit: 3000 # (usually can be found on the official website). +- id: trst + blockchain_key: eth-rinkeby + symbol: 'Ξ' + type: coin + precision: 8 + base_factor: 1_000_000 # IMPORTANT: Don't forget to update this variable according + enabled: true # to your ERC20-based currency requirements + quick_withdraw_limit: 3000 # (usually can be found on the official website). # Deposits with less amount are skipped during blockchain synchronization. # We advise to set value 10 times bigger than the network fee to prevent losses. # NOTE: Network fee is paid in ETH but min_deposit_amount is in TRST. - min_deposit_amount: 1.7 - deposit_fee: 0 - withdraw_fee: 0 + min_deposit_amount: 0.00021 + min_collection_amount: 0.00021 + deposit_fee: 0 + withdraw_fee: 0 options: # # ERC20 configuration. diff --git a/db/migrate/20181017114624_enumerize_wallet_kind.rb b/db/migrate/20181017114624_enumerize_wallet_kind.rb new file mode 100644 index 0000000000..d14ef62f10 --- /dev/null +++ b/db/migrate/20181017114624_enumerize_wallet_kind.rb @@ -0,0 +1,15 @@ +class EnumerizeWalletKind < ActiveRecord::Migration + def change + id_kind_hash = Wallet.pluck(:id, :kind).to_h + + remove_column :wallets, :kind + add_column :wallets, :kind, :integer, limit: 4, null: false, after: :address + + Wallet.find_each { |w| w.update!(kind: id_kind_hash[w.id]) } + + add_index :wallets, :status + add_index :wallets, :kind + add_index :wallets, :currency_id + add_index :wallets, %i[kind currency_id status] + end +end diff --git a/db/migrate/20181126101312_add_min_collection_amount_to_currencies.rb b/db/migrate/20181126101312_add_min_collection_amount_to_currencies.rb new file mode 100644 index 0000000000..c79408d4db --- /dev/null +++ b/db/migrate/20181126101312_add_min_collection_amount_to_currencies.rb @@ -0,0 +1,5 @@ +class AddMinCollectionAmountToCurrencies < ActiveRecord::Migration + def change + add_column :currencies, :min_collection_amount, :decimal, precision: 32, scale: 16, default: 0.0, null: false, after: :min_deposit_amount + end +end diff --git a/db/schema.rb b/db/schema.rb index 46be0fca8d..2022933c04 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20181004114428) do +ActiveRecord::Schema.define(version: 20181126101312) do create_table "accounts", force: :cascade do |t| t.integer "member_id", limit: 4, null: false @@ -57,20 +57,21 @@ add_index "blockchains", ["status"], name: "index_blockchains_on_status", using: :btree create_table "currencies", force: :cascade do |t| - t.string "blockchain_key", limit: 32 - t.string "symbol", limit: 1, null: false - t.string "type", limit: 30, default: "coin", null: false - t.decimal "deposit_fee", precision: 32, scale: 16, default: 0.0, null: false - t.decimal "quick_withdraw_limit", precision: 32, scale: 16, default: 0.0, null: false - t.decimal "min_deposit_amount", precision: 32, scale: 16, default: 0.0, null: false - t.decimal "withdraw_fee", precision: 32, scale: 16, default: 0.0, null: false - t.string "options", limit: 1000, default: "{}", null: false - t.boolean "enabled", default: true, null: false - t.integer "base_factor", limit: 8, default: 1, null: false - t.integer "precision", limit: 1, default: 8, null: false - t.string "icon_url", limit: 255 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "blockchain_key", limit: 32 + t.string "symbol", limit: 1, null: false + t.string "type", limit: 30, default: "coin", null: false + t.decimal "deposit_fee", precision: 32, scale: 16, default: 0.0, null: false + t.decimal "quick_withdraw_limit", precision: 32, scale: 16, default: 0.0, null: false + t.decimal "min_deposit_amount", precision: 32, scale: 16, default: 0.0, null: false + t.decimal "min_collection_amount", precision: 32, scale: 16, default: 0.0, null: false + t.decimal "withdraw_fee", precision: 32, scale: 16, default: 0.0, null: false + t.string "options", limit: 1000, default: "{}", null: false + t.boolean "enabled", default: true, null: false + t.integer "base_factor", limit: 8, default: 1, null: false + t.integer "precision", limit: 1, default: 8, null: false + t.string "icon_url", limit: 255 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end add_index "currencies", ["enabled"], name: "index_currencies_on_enabled", using: :btree @@ -197,7 +198,7 @@ t.string "currency_id", limit: 10 t.string "name", limit: 64 t.string "address", limit: 255, null: false - t.string "kind", limit: 32, null: false + t.integer "kind", limit: 4, null: false t.integer "nsig", limit: 4 t.string "gateway", limit: 20, default: "", null: false t.string "settings", limit: 1000, default: "{}", null: false @@ -208,6 +209,11 @@ t.datetime "updated_at", null: false end + add_index "wallets", ["currency_id"], name: "index_wallets_on_currency_id", using: :btree + add_index "wallets", ["kind", "currency_id", "status"], name: "index_wallets_on_kind_and_currency_id_and_status", using: :btree + add_index "wallets", ["kind"], name: "index_wallets_on_kind", using: :btree + add_index "wallets", ["status"], name: "index_wallets_on_status", using: :btree + create_table "withdraws", force: :cascade do |t| t.integer "account_id", limit: 4, null: false t.integer "member_id", limit: 4, null: false diff --git a/lib/blockchain_client/bitcoin.rb b/lib/blockchain_client/bitcoin.rb index 6b0957c440..d0700e03eb 100644 --- a/lib/blockchain_client/bitcoin.rb +++ b/lib/blockchain_client/bitcoin.rb @@ -13,6 +13,12 @@ def endpoint @json_rpc_endpoint end + def load_balance!(address, currency) + json_rpc(:listunspent, [1, 10_000_000, [address]]) + .fetch('result') + .sum { |vout| vout['amount'] } + end + def load_deposit!(txid) json_rpc(:gettransaction, [normalize_txid(txid)]).fetch('result').yield_self { |tx| build_standalone_deposit(tx) } end diff --git a/lib/blockchain_client/ethereum.rb b/lib/blockchain_client/ethereum.rb index 0a18f34a23..9ec2a6dc44 100644 --- a/lib/blockchain_client/ethereum.rb +++ b/lib/blockchain_client/ethereum.rb @@ -22,6 +22,27 @@ def get_block(height) json_rpc(:eth_getBlockByNumber, ["0x#{current_block.to_s(16)}", true]).fetch('result') end + def load_balance!(address, currency) + if currency.code.eth? + json_rpc(:eth_getBalance, [normalize_address(address), 'latest']) + .fetch('result') + .hex + .to_d + .yield_self { |amount| convert_from_base_unit(amount, currency) } + else + load_balance_of_address(address, currency) + end + end + + def load_balance_of_address(address, currency) + data = abi_encode('balanceOf(address)', normalize_address(address)) + json_rpc(:eth_call, [{ to: contract_address(currency), data: data }, 'latest']) + .fetch('result') + .hex + .to_d + .yield_self { |amount| convert_from_base_unit(amount, currency) } + end + def to_address(tx) if tx.has_key?('logs') get_erc20_addresses(tx) @@ -75,6 +96,10 @@ def case_sensitive? false end + def convert_from_base_unit(value, currency) + value.to_d / currency.base_factor + end + protected def connection @@ -172,7 +197,7 @@ def build_erc20_transaction(tx, current_block_json, address, currency) entries: entries.compact } end - def contract_address + def contract_address(currency) normalize_address(currency.erc20_contract_address) end end diff --git a/lib/blockchain_client/ripple.rb b/lib/blockchain_client/ripple.rb index 6e3674a70a..9d63073817 100644 --- a/lib/blockchain_client/ripple.rb +++ b/lib/blockchain_client/ripple.rb @@ -21,6 +21,18 @@ def from_address(tx) normalize_address(tx['Account']) end + def load_balance!(address, currency) + json_rpc(:account_info, [account: normalize_address(address), ledger_index: 'validated', strict: true]) + .fetch('result') + .fetch('account_data') + .fetch('Balance') + .to_d + .yield_self { |amount| convert_from_base_unit(amount, currency) } + rescue => e + report_exception_to_screen(e) + 0.0 + end + def build_transaction(tx:, currency:) { id: normalize_txid(tx.fetch('hash')), diff --git a/lib/wallet_client/geth.rb b/lib/wallet_client/geth.rb index 8fa3684e46..e0897a09f3 100644 --- a/lib/wallet_client/geth.rb +++ b/lib/wallet_client/geth.rb @@ -82,6 +82,13 @@ def normalize_txid(txid) txid.downcase end + def load_balance!(address) + json_rpc(:eth_getBalance, [normalize_address(address), 'latest']).fetch('result').hex.to_d + rescue => e + report_exception_to_screen(e) + 0.0 + end + protected def abi_encode(method, *args) diff --git a/lib/wallet_client/rippled.rb b/lib/wallet_client/rippled.rb index 8ab7fbcb2b..59584b0cea 100644 --- a/lib/wallet_client/rippled.rb +++ b/lib/wallet_client/rippled.rb @@ -112,7 +112,19 @@ def calculate_current_fee end end - protected + def load_balance!(address) + json_rpc(:account_info, [account: normalize_address(address), ledger_index: 'validated', strict: true]) + .fetch('result') + .fetch('account_data') + .fetch('Balance') + .to_d + .yield_self { |amount| convert_from_base_unit(amount) } + rescue => e + report_exception_to_screen(e) + 0.0 + end + + protected def connection Faraday.new(@json_rpc_endpoint).tap do |connection| diff --git a/spec/factories/blockchains.rb b/spec/factories/blockchains.rb index 132260c28f..c15fbe3309 100644 --- a/spec/factories/blockchains.rb +++ b/spec/factories/blockchains.rb @@ -7,7 +7,7 @@ key { 'xrp-testnet' } name { 'Ripple Testnet' } client { 'ripple' } - server { 'https://s.altnet.rippletest.net:51234' } + server { 'http://127.0.0.1:5005' } height { 40280751 } min_confirmations { 1 } explorer_address { '' } @@ -46,7 +46,7 @@ server { 'http://127.0.0.1:18332' } height { 1350000 } min_confirmations { 1 } - explorer_address { ' https://blockchain.info/address/#{address}' } + explorer_address { 'https://blockchain.info/address/#{address}' } explorer_transaction { 'https://blockchain.info/tx/#{txid}' } status { 'active' } end diff --git a/spec/factories/deposits.rb b/spec/factories/deposits.rb index b14b84e104..52bec42295 100644 --- a/spec/factories/deposits.rb +++ b/spec/factories/deposits.rb @@ -6,15 +6,74 @@ member { create(:member, :level_3) } amount { Kernel.rand(100..10_000).to_d } - factory :deposit_btc, class: 'Deposits::Coin' do + factory :deposit_btc, class: Deposits::Coin do currency { Currency.find(:btc) } address { Faker::Bitcoin.address } txid { Faker::Lorem.characters(64) } txout { 0 } end - factory :deposit_usd, class: 'Deposits::Fiat' do + factory :deposit_usd, class: Deposits::Fiat do currency { Currency.find(:usd) } end + + trait :deposit_btc do + type { Deposits::Coin } + currency { Currency.find(:btc) } + address { Faker::Bitcoin.address } + txid { Faker::Lorem.characters(64) } + txout { 0 } + end + + trait :deposit_bch do + type { Deposits::Coin } + currency { Currency.find(:bch) } + address { Faker::Bitcoin.address } + txid { Faker::Lorem.characters(64) } + txout { 0 } + end + + trait :deposit_dash do + type { Deposits::Coin } + currency { Currency.find(:dash) } + address { Faker::Bitcoin.address } + txid { Faker::Lorem.characters(64) } + txout { 0 } + end + + trait :deposit_ltc do + type { Deposits::Coin } + currency { Currency.find(:ltc) } + address { Faker::Bitcoin.address } + txid { Faker::Lorem.characters(64) } + txout { 0 } + end + + trait :deposit_eth do + type { Deposits::Coin } + currency { Currency.find(:eth) } + member { create(:member, :level_3, :barong) } + address { Faker::Bitcoin.address } + txid { Faker::Lorem.characters(64) } + txout { 0 } + end + + trait :deposit_trst do + type { Deposits::Coin } + currency { Currency.find(:trst) } + member { create(:member, :level_3, :barong) } + address { Faker::Bitcoin.address } + txid { Faker::Lorem.characters(64) } + txout { 0 } + end + + trait :deposit_xrp do + type { Deposits::Coin } + currency { Currency.find(:xrp) } + member { create(:member, :level_3, :barong) } + address { Faker::Bitcoin.address } + txid { Faker::Lorem.characters(64) } + txout { 0 } + end end end diff --git a/spec/factories/wallet.rb b/spec/factories/wallet.rb index 19fe6accdb..8211a999c8 100644 --- a/spec/factories/wallet.rb +++ b/spec/factories/wallet.rb @@ -3,11 +3,26 @@ FactoryBot.define do factory :wallet do + + trait :eth_deposit do + currency_id { 'eth' } + blockchain_key { 'eth-rinkeby' } + name { 'Ethereum Deposit Wallet' } + address { '0x828058628DF254Ebf252e0b1b5393D1DED91E369' } + kind { 'deposit' } + max_balance { 0.0 } + nsig { 2 } + status { 'active' } + gateway { 'geth' } + uri { 'http://127.0.0.1:8545' } + secret { 'changeme' } + end + trait :eth_hot do currency_id { 'eth' } blockchain_key { 'eth-rinkeby' } name { 'Ethereum Hot Wallet' } - address { '249048804499541338815845805798634312140346616732' } + address { '0xb6a61c43DAe37c0890936D720DC42b5CBda990F9' } kind { 'hot' } max_balance { 100.0 } nsig { 2 } @@ -17,12 +32,13 @@ secret { 'changeme' } end - trait 'eth_warm' do + trait :eth_warm do currency_id { 'eth' } blockchain_key { 'eth-rinkeby' } name { 'Ethereum Warm Wallet' } address { '0x2b9fBC10EbAeEc28a8Fc10069C0BC29E45eBEB9C' } kind { 'warm' } + max_balance { 1000.0 } nsig { 2 } status { 'active' } gateway { 'geth' } @@ -30,16 +46,59 @@ secret { 'changeme' } end - trait :btc_hot do - currency_id { 'btc' } - blockchain_key { 'btc-testnet' } - name { 'Bitcoin Hot Wallet' } + trait :eth_cold do + currency_id { 'eth' } + blockchain_key { 'eth-rinkeby' } + name { 'Ethereum Cold Wallet' } address { '0x2b9fBC10EbAeEc28a8Fc10069C0BC29E45eBEB9C' } + kind { 'cold' } + max_balance { 1000.0 } + nsig { 2 } + status { 'active' } + gateway { 'geth' } + uri { 'http://127.0.0.1:8545' } + secret { 'changeme' } + end + + trait :eth_fee do + currency_id { 'eth' } + blockchain_key { 'eth-rinkeby' } + name { 'Ethereum Fee Wallet' } + address { '0x45a31b15a2ab8a8477375b36b6f5a0c63733dce8' } + kind { 'fee' } + max_balance { 1000.0 } + nsig { 2 } + status { 'active' } + gateway { 'geth' } + uri { 'http://127.0.0.1:8545' } + secret { 'changeme' } + end + + trait :trst_deposit do + currency_id { 'trst' } + blockchain_key { 'eth-rinkeby' } + name { 'Trust Coin Deposit Wallet' } + address { '0x828058628DF254Ebf252e0b1b5393D1DED91E369' } + kind { 'deposit' } + max_balance { 0.0 } + nsig { 2 } + status { 'active' } + gateway { 'geth' } + uri { 'http://127.0.0.1:8545' } + secret { 'changeme' } + end + + trait :trst_hot do + currency_id { 'trst' } + blockchain_key { 'eth-rinkeby' } + name { 'Trust Coin Hot Wallet' } + address { '0xb6a61c43DAe37c0890936D720DC42b5CBda990F9' } kind { 'hot' } + max_balance { 100.0 } nsig { 2 } status { 'active' } - gateway { 'bitcoind' } - uri { 'http://127.0.0.1:18332' } + gateway { 'geth' } + uri { 'http://127.0.0.1:8545' } secret { 'changeme' } end @@ -47,8 +106,9 @@ currency_id { 'btc' } blockchain_key { 'btc-testnet' } name { 'Bitcoin Deposit Wallet' } - address { '0x2b9fBC10EbAeEc28a8Fc10069C0BC29E45eBEB9C' } + address { '3DX3Ak4751ckkoTFbYSY9FEQ6B7mJ4furT' } kind { 'deposit' } + max_balance { 0.0 } nsig { 2 } status { 'active' } gateway { 'bitcoind' } @@ -56,17 +116,128 @@ secret { 'changeme' } end + trait :btc_hot do + currency_id { 'btc' } + blockchain_key { 'btc-testnet' } + name { 'Bitcoin Hot Wallet' } + address { '3NwYr8JxjHG2MBkgdBiHCxStSWDzyjS5U8' } + kind { 'hot' } + max_balance { 500.0 } + nsig { 2 } + status { 'active' } + gateway { 'bitcoind' } + uri { 'http://127.0.0.1:18332' } + secret { 'changeme' } + end + + trait :xrp_deposit do + currency_id { 'xrp' } + blockchain_key { 'xrp-testnet' } + name { 'Ripple Deposit Wallet' } + address { 'rN3J1yMz2PCGievtS2XTEgkrmdHiJgzb5Y?dt=917590223' } + kind { 'deposit' } + max_balance { 0.0 } + nsig { 2 } + status { 'active' } + gateway { 'rippled' } + uri { 'http://127.0.0.1:5005' } + secret { 'changeme' } + end + trait :xrp_hot do currency_id { 'xrp' } blockchain_key { 'xrp-testnet' } name { 'Ripple Hot Wallet' } address { 'r4kpJtnx4goLYXoRdi7mbkRpZ9Xpx2RyPN' } kind { 'hot' } + max_balance { 100.0 } nsig { 2 } status { 'active' } gateway { 'rippled' } uri { 'http://127.0.0.1:5005' } secret { 'changeme' } end + + trait :bch_deposit do + currency_id { 'bch' } + blockchain_key { 'bch-testnet' } + name { 'Bitcoincash Deposit Wallet' } + address { 'mqF8Bsv2rHThg4cVDgwYcnEYNDWKi4spD7' } + kind { 'deposit' } + max_balance { 0.0 } + nsig { 1 } + status { 'active' } + gateway { 'bitcoincashd' } + uri { 'http://127.0.0.1:18332' } + secret { 'changeme' } + end + + trait :bch_hot do + currency_id { 'bch' } + blockchain_key { 'bch-testnet' } + name { 'Bitcoincash Hot Wallet' } + address { 'n2stP7w1DpSh7N1PzJh7eGjgCk3eTF3DMC' } + kind { 'hot' } + max_balance { 100.0 } + nsig { 1 } + status { 'active' } + gateway { 'bitcoincashd' } + uri { 'http://127.0.0.1:18332' } + secret { 'changeme' } + end + + trait :dash_deposit do + currency_id { 'dash' } + blockchain_key { 'dash-testnet' } + name { 'Dash Deposit Wallet' } + address { 'yVcZM6oUjfwrREm2CDb9G8BMHwwm5o5UsL' } + kind { 'deposit' } + max_balance { 0.0 } + nsig { 1 } + status { 'active' } + gateway { 'dashd' } + uri { 'http://127.0.0.1:19998' } + secret { 'changeme' } + end + + trait :dash_hot do + currency_id { 'dash' } + blockchain_key { 'dash-testnet' } + name { 'Dash Hot Wallet' } + address { 'yborj44WhothaX6vwoMhRMjkq1xELhAWQp' } + kind { 'hot' } + max_balance { 100.0 } + nsig { 1 } + status { 'active' } + gateway { 'dashd' } + uri { 'http://127.0.0.1:19998' } + secret { 'changeme' } + end + + trait :ltc_deposit do + currency_id { 'ltc' } + blockchain_key { 'ltc-testnet' } + name { 'Litecoin Deposit Wallet' } + address { 'QcM2zjgbaXbH26utxnNFge24A1BnDgSgcU' } + kind { 'deposit' } + max_balance { 0.0 } + nsig { 1 } + status { 'active' } + gateway { 'litecoind' } + uri { 'http://127.0.0.1:17732' } + end + + trait :ltc_hot do + currency_id { 'ltc' } + blockchain_key { 'ltc-testnet' } + name { 'Litecoin Hot Wallet' } + address { 'Qc2BM7gp8mKgJPPxLAadLAHteNQwhFwwuf' } + kind { 'hot' } + max_balance { 100.0 } + nsig { 1 } + status { 'active' } + gateway { 'litecoind' } + uri { 'http://127.0.0.1:17732' } + end end end diff --git a/spec/models/wallet_spec.rb b/spec/models/wallet_spec.rb index cf94d205d0..926f5ef35f 100644 --- a/spec/models/wallet_spec.rb +++ b/spec/models/wallet_spec.rb @@ -4,7 +4,7 @@ describe Wallet do context 'validations' do - subject { build(:wallet, 'eth_warm') } + subject { build(:wallet, :eth_cold) } it 'checks valid record' do expect(subject).to be_valid diff --git a/spec/resources/ripple-data/sign-transaction.json b/spec/resources/ripple-data/sign-transaction.json new file mode 100644 index 0000000000..318428bebf --- /dev/null +++ b/spec/resources/ripple-data/sign-transaction.json @@ -0,0 +1,22 @@ +{ + "result": { + "status": "success", + "tx_blob": "1200002280000000240000016861D4838D7EA4C6800000000000000000000000000055534400000000004B4E9C06F24296074F7BC48F92A97916C6DC5EA9684000000000002710732103AB40A0490F9B7ED8DF29D246BF2D6269820A0EE7742ACDD457BEA7C7D0931EDB7446304402200E5C2DD81FDF0BE9AB2A8D797885ED49E804DBF28E806604D878756410CA98B102203349581946B0DDA06B36B35DBC20EDA27552C1F167BCF5C6ECFF49C6A46F858081144B4E9C06F24296074F7BC48F92A97916C6DC5EA983143E9D4A2B8AA0780F682D136F7A56D6724EF53754", + "tx_json": { + "Account": "rN3J1yMz2PCGievtS2XTEgkrmdHiJgzb5Y", + "Amount": { + "currency": "XRP", + "issuer": "rN3J1yMz2PCGievtS2XTEgkrmdHiJgzb5Y", + "value": "10000000" + }, + "Destination": "r4kpJtnx4goLYXoRdi7mbkRpZ9Xpx2RyPN", + "Fee": "10000", + "Flags": 2147483648, + "Sequence": 360, + "SigningPubKey": "03AB40A0490F9B7ED8DF29D246BF2D6269820A0EE7742ACDD457BEA7C7D0931EDB", + "TransactionType": "Payment", + "TxnSignature": "304402200E5C2DD81FDF0BE9AB2A8D797885ED49E804DBF28E806604D878756410CA98B102203349581946B0DDA06B36B35DBC20EDA27552C1F167BCF5C6ECFF49C6A46F8580", + "hash": "4D5D90890F8D49519E4151938601EF3D0B30B16CD6A519D9C99102C9FA77F7E0" + } + } +} diff --git a/spec/resources/ripple-data/submit-transaction.json b/spec/resources/ripple-data/submit-transaction.json new file mode 100644 index 0000000000..ceac2e1822 --- /dev/null +++ b/spec/resources/ripple-data/submit-transaction.json @@ -0,0 +1,25 @@ +{ + "result": { + "engine_result": "tesSUCCESS", + "engine_result_code": 0, + "engine_result_message": "The transaction was applied. Only final in a validated ledger.", + "status": "success", + "tx_blob": "1200002280000000240000016961D4838D7EA4C6800000000000000000000000000055534400000000004B4E9C06F24296074F7BC48F92A97916C6DC5EA9684000000000002710732103AB40A0490F9B7ED8DF29D246BF2D6269820A0EE7742ACDD457BEA7C7D0931EDB74473045022100A7CCD11455E47547FF617D5BFC15D120D9053DFD0536B044F10CA3631CD609E502203B61DEE4AC027C5743A1B56AF568D1E2B8E79BB9E9E14744AC87F38375C3C2F181144B4E9C06F24296074F7BC48F92A97916C6DC5EA983143E9D4A2B8AA0780F682D136F7A56D6724EF53754", + "tx_json": { + "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Amount": { + "currency": "USD", + "issuer": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "value": "1" + }, + "Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + "Fee": "10000", + "Flags": 2147483648, + "Sequence": 361, + "SigningPubKey": "03AB40A0490F9B7ED8DF29D246BF2D6269820A0EE7742ACDD457BEA7C7D0931EDB", + "TransactionType": "Payment", + "TxnSignature": "3045022100A7CCD11455E47547FF617D5BFC15D120D9053DFD0536B044F10CA3631CD609E502203B61DEE4AC027C5743A1B56AF568D1E2B8E79BB9E9E14744AC87F38375C3C2F1", + "hash": "5B31A7518DC304D5327B4887CD1F7DC2C38D5F684170097020C7C9758B973847" + } + } +} diff --git a/spec/services/blockchain_service/bitcoin_spec.rb b/spec/services/blockchain_service/bitcoin_spec.rb index cfaf9796cd..035af8a3e7 100644 --- a/spec/services/blockchain_service/bitcoin_spec.rb +++ b/spec/services/blockchain_service/bitcoin_spec.rb @@ -69,6 +69,8 @@ def request_block_body(block_hash) # Mock requests and methods. client.class.any_instance.stubs(:latest_block_number).returns(latest_block) + Deposits::Coin.where(currency: currency).delete_all + block_data.each_with_index do |blk, index| # stub get_block_hash stub_request(:post, client.endpoint) @@ -224,6 +226,8 @@ def request_block_body(block_hash) # Mock requests and methods. client.class.any_instance.stubs(:latest_block_number).returns(latest_block) + Deposits::Coin.where(currency: currency).delete_all + block_data.each_with_index do |blk, index| # stub get_block_hash stub_request(:post, client.endpoint) diff --git a/spec/services/blockchain_service/bitcoincash_spec.rb b/spec/services/blockchain_service/bitcoincash_spec.rb index b7557b98a5..4dfc4c4eed 100644 --- a/spec/services/blockchain_service/bitcoincash_spec.rb +++ b/spec/services/blockchain_service/bitcoincash_spec.rb @@ -83,6 +83,8 @@ def request_raw_transaction_body(txid) # Mock requests and methods. client.class.any_instance.stubs(:latest_block_number).returns(latest_block) + Deposits::Coin.where(currency: currency).delete_all + block_data.each_with_index do |blk, index| # stub get_block_hash stub_request(:post, client.endpoint) @@ -166,6 +168,8 @@ def request_raw_transaction_body(txid) # Mock requests and methods. client.class.any_instance.stubs(:latest_block_number).returns(latest_block) + Deposits::Coin.where(currency: currency).delete_all + block_data.each_with_index do |blk, index| # stub get_block_hash stub_request(:post, client.endpoint) diff --git a/spec/services/blockchain_service/dash_spec.rb b/spec/services/blockchain_service/dash_spec.rb index e168c393ce..8ce9629389 100644 --- a/spec/services/blockchain_service/dash_spec.rb +++ b/spec/services/blockchain_service/dash_spec.rb @@ -77,6 +77,7 @@ def request_raw_transaction_body(txid) before do # Mock requests and methods. client.class.any_instance.stubs(:latest_block_number).returns(latest_block) + Deposits::Coin.where(currency: currency).delete_all block_data.each_with_index do |blk, index| # stub get_block_hash @@ -160,6 +161,7 @@ def request_raw_transaction_body(txid) before do # Mock requests and methods. client.class.any_instance.stubs(:latest_block_number).returns(latest_block) + Deposits::Coin.where(currency: currency).delete_all block_data.each_with_index do |blk, index| # stub get_block_hash diff --git a/spec/services/blockchain_service/ethereum_spec.rb b/spec/services/blockchain_service/ethereum_spec.rb index 9da84e9289..0995c29d40 100644 --- a/spec/services/blockchain_service/ethereum_spec.rb +++ b/spec/services/blockchain_service/ethereum_spec.rb @@ -74,6 +74,8 @@ def request_body(block_number, index) client.class.any_instance.stubs(:latest_block_number).returns(latest_block) client.class.any_instance.stubs(:rpc_call_id).returns(1) + Deposits::Coin.where(currency: currency).delete_all + block_data.each_with_index do |blk, index| stub_request(:post, client.endpoint) .with(body: request_body(blk['result']['number'],index)) @@ -151,6 +153,8 @@ def request_body(block_number, index) client.class.any_instance.stubs(:latest_block_number).returns(latest_block) client.class.any_instance.stubs(:rpc_call_id).returns(1) + Deposits::Coin.where(currency: currency).delete_all + block_data.each_with_index do |blk, index| stub_request(:post, client.endpoint) .with(body: request_body(blk['result']['number'], index)) diff --git a/spec/services/blockchain_service/litecoin_spec.rb b/spec/services/blockchain_service/litecoin_spec.rb index f814d0f819..30f8dc347b 100644 --- a/spec/services/blockchain_service/litecoin_spec.rb +++ b/spec/services/blockchain_service/litecoin_spec.rb @@ -63,6 +63,7 @@ def request_block_body(block_hash) before do # Mock requests and methods. client.class.any_instance.stubs(:latest_block_number).returns(latest_block) + Deposits::Coin.where(currency: currency).delete_all block_data.each_with_index do |blk, index| # stub get_block_hash @@ -143,6 +144,7 @@ def request_block_body(block_hash) before do # Mock requests and methods. client.class.any_instance.stubs(:latest_block_number).returns(latest_block) + Deposits::Coin.where(currency: currency).delete_all block_data.each_with_index do |blk, index| # stub get_block_hash diff --git a/spec/services/blockchain_service/ripple_spec.rb b/spec/services/blockchain_service/ripple_spec.rb index 0fbfb86af5..0b867a69c3 100644 --- a/spec/services/blockchain_service/ripple_spec.rb +++ b/spec/services/blockchain_service/ripple_spec.rb @@ -116,6 +116,7 @@ def request_body(ledger_index, index) end before do + Deposits::Coin.where(currency: currency).delete_all client.class.any_instance.stubs(:latest_block_number).returns(latest_block_number) stub_request(:post, client.endpoint) .with(body: request_body(start_ledger_index, 0)) diff --git a/spec/services/wallet_service/bitcoincashd_spec.rb b/spec/services/wallet_service/bitcoincashd_spec.rb new file mode 100644 index 0000000000..5cc04e7eac --- /dev/null +++ b/spec/services/wallet_service/bitcoincashd_spec.rb @@ -0,0 +1,101 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +describe WalletService::Bitcoincashd do + + around do |example| + WebMock.disable_net_connect! + example.run + WebMock.allow_net_connect! + end + + describe 'WalletService::Bitcoincashd' do + + let(:deposit) { create(:deposit, :deposit_bch, amount: 10) } + let(:withdraw) { create(:bch_withdraw) } + let(:deposit_wallet) { Wallet.find_by(gateway: :bitcoincashd, kind: :deposit) } + let(:hot_wallet) { Wallet.find_by(gateway: :bitcoincashd, kind: :hot) } + + context '#create_address' do + subject { WalletService[deposit_wallet].create_address } + + let(:new_address) { 'bchtest:pzsze7ety982w764sh9nq2ztknz0988w9cp7sv0zpj' } + + let :getnewaddress_request do + { jsonrpc: '1.0', + method: 'getnewaddress', + params: [] + }.to_json + end + + let :getnewaddress_response do + { result: new_address }.to_json + end + + before do + stub_request(:post, deposit_wallet.uri).with(body: getnewaddress_request).to_return(body: getnewaddress_response) + end + + it { is_expected.to eq(address: new_address) } + end + + context '#collect_deposit!' do + subject { WalletService[deposit_wallet].collect_deposit!(deposit) } + + let(:txid) { 'dcedf50780f251c99e748362c1a035f2916efb9bb44fe5c5c3e857ea74ca06b3' } + + let :listunspent_request do + { + jsonrpc: '1.0', + method: 'listunspent', + params: [1, 10000000, ['bchtest:qr49q8zvd3w6yeteak3hsap36s0ywpaj9g022qu4aw']], + }.to_json + end + + let :listunspent_response do + { result: '0' }.to_json + end + + let :sendtoaddress_request do + { jsonrpc: '1.0', + method: 'sendtoaddress', + params: [hot_wallet.address, deposit.amount, '', '', true] + }.to_json + end + + let :sendtoaddress_response do + { result: txid }.to_json + end + + before do + stub_request(:post, hot_wallet.uri).with(body: listunspent_request).to_return(body: listunspent_response) + stub_request(:post, deposit_wallet.uri).with(body: sendtoaddress_request).to_return(body: sendtoaddress_response) + end + + it { is_expected.to eq([txid]) } + end + + context '#build_withdrawal!' do + subject { WalletService[hot_wallet].build_withdrawal!(withdraw) } + + let(:txid) { 'dcedf50780f251c99e748362c1a035f2916efb9bb44fe5c5c3e857ea74ca06b3' } + + let :sendtoaddress_request do + { jsonrpc: '1.0', + method: 'sendtoaddress', + params: [withdraw.rid, withdraw.amount, '', '', false] + }.to_json + end + + let :sendtoaddress_response do + { result: txid }.to_json + end + + before do + stub_request(:post, hot_wallet.uri).with(body: sendtoaddress_request).to_return(body: sendtoaddress_response) + end + + it { is_expected.to eq(txid) } + end + end +end diff --git a/spec/services/wallet_service/bitcoind_spec.rb b/spec/services/wallet_service/bitcoind_spec.rb new file mode 100644 index 0000000000..7113408ac3 --- /dev/null +++ b/spec/services/wallet_service/bitcoind_spec.rb @@ -0,0 +1,99 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +describe WalletService::Bitcoind do + around do |example| + WebMock.disable_net_connect! + example.run + WebMock.allow_net_connect! + end + + describe 'WalletService::Bitcoind' do + + let(:deposit) { create(:deposit, :deposit_btc, amount: 10) } + let(:withdraw) { create(:btc_withdraw) } + let(:deposit_wallet) { Wallet.find_by(gateway: :bitcoind, kind: :deposit) } + let(:hot_wallet) { Wallet.find_by(gateway: :bitcoind, kind: :hot) } + + context '#create_address' do + subject { WalletService[deposit_wallet].create_address } + + let(:newaddress) { '2N7r9zKXkypzqtXfWkKfs3uZqKbJUhdK6JE' } + let :getnewaddress_request do + { jsonrpc: '1.0', + method: 'getnewaddress', + params: [] + }.to_json + end + + let :getnewaddress_response do + { result: newaddress }.to_json + end + + before do + stub_request(:post, deposit_wallet.uri).with(body: getnewaddress_request).to_return(body: getnewaddress_response) + end + + it { is_expected.to eq(address: newaddress) } + end + + context '#collect_deposit!' do + subject { WalletService[deposit_wallet].collect_deposit!(deposit) } + + let(:txid) { 'dcedf50780f251c99e748362c1a035f2916efb9bb44fe5c5c3e857ea74ca06b3' } + + let :listunspent_response do + { result: '0' }.to_json + end + + let :listunspent_request do + { + jsonrpc: '1.0', + method: 'listunspent', + params: [1, 10000000, ['3NwYr8JxjHG2MBkgdBiHCxStSWDzyjS5U8']], + }.to_json + end + + let :sendtoaddress_request do + { jsonrpc: '1.0', + method: 'sendtoaddress', + params: [hot_wallet.address, deposit.amount, '', '', true] + }.to_json + end + + let :sendtoaddress_response do + { result: txid }.to_json + end + + before do + stub_request(:post, hot_wallet.uri).with(body: listunspent_request).to_return(body: listunspent_response) + stub_request(:post, deposit_wallet.uri).with(body: sendtoaddress_request).to_return(body: sendtoaddress_response) + end + + it { is_expected.to eq([txid]) } + end + + context '#build_withdrawal!' do + subject { WalletService[hot_wallet].build_withdrawal!(withdraw) } + + let(:txid) { 'dcedf50780f251c99e748362c1a035f2916efb9bb44fe5c5c3e857ea74ca06b3' } + + let :sendtoaddress_request do + { jsonrpc: '1.0', + method: 'sendtoaddress', + params: [withdraw.rid, withdraw.amount, '', '', false] + }.to_json + end + + let :sendtoaddress_response do + { result: txid }.to_json + end + + before do + stub_request(:post, hot_wallet.uri).with(body: sendtoaddress_request).to_return(body: sendtoaddress_response) + end + + it { is_expected.to eq(txid) } + end + end +end diff --git a/spec/services/wallet_service/bitgo_spec.rb b/spec/services/wallet_service/bitgo_spec.rb new file mode 100644 index 0000000000..3a3cd5158a --- /dev/null +++ b/spec/services/wallet_service/bitgo_spec.rb @@ -0,0 +1,123 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +describe WalletService::Bitgo do + around do |example| + WebMock.disable_net_connect! + example.run + WebMock.allow_net_connect! + end + + let(:deposit) { create(:deposit_btc) } + let(:deposit_wallet) { Wallet.find_by(currency: :btc, kind: :deposit) } + let(:hot_wallet) { Wallet.find_by(currency: :btc, kind: :hot) } + let(:wallet_client) { WalletClient[deposit_wallet] } + let(:withdraw) { create(:btc_withdraw) } + + before do + [deposit_wallet, hot_wallet].each do |wallet| + wallet.update! \ + gateway: 'bitgo', + bitgo_test_net: true, + bitgo_wallet_id: '5a7d9f52ba1923b107b80baabe0c3574', + address: '2MtmgqDM5Gb91dAo1cUHpx9fdh1xgD7L1Xb', + bitgo_wallet_passphrase: 'secret', + bitgo_rest_api_root: 'http://127.0.0.1:3080/api/v2', + bitgo_rest_api_access_token: 'v2x0b53e612518e5ea625eb3c24175438b37f56bc1f82e9c9ba3b038c91b0c72e67' + end + end + + def request_headers(wallet) + { Accept: 'application/json', + Authorization: 'Bearer ' + wallet.bitgo_rest_api_access_token } + end + + def response_headers + { 'Content-Type' => 'application/json' } + end + + describe '#create_address' do + subject { WalletService[deposit_wallet].create_address(options) } + + before do + stub_request(request_method, deposit_wallet.bitgo_rest_api_root + request_path) + .with(body: request_body, headers: request_headers(deposit_wallet)) + .to_return(status: 200, body: response_body, headers: response_headers) + end + + let(:request_body) { {} } + let(:response_body) { '{"id":"5acb44423a713ade07b42b0140f91a96","address":"2MySruptM4SgZF49KSc3x5KyxAW61ghyvtc"}' } + + context 'when BitGo address ID is provided' do + let(:options) { {} } + let(:request_method) { :post } + let(:request_path) { '/tbtc/wallet/' + deposit_wallet.bitgo_wallet_id + '/address' } + + it { is_expected.to eq(address: '2MySruptM4SgZF49KSc3x5KyxAW61ghyvtc', bitgo_address_id: '5acb44423a713ade07b42b0140f91a96') } + end + + context 'when BitGo address ID is provided' do + let(:request_path) { '/tbtc/wallet/' + deposit_wallet.bitgo_wallet_id + '/address/5acb44423a713ade07b42b0140f91a96' } + let(:request_method) { :get } + let(:options) { { address_id: '5acb44423a713ade07b42b0140f91a96' } } + + it { is_expected.to eq(address: '2MySruptM4SgZF49KSc3x5KyxAW61ghyvtc') } + end + end + + describe '#collect_deposit!' do + subject { WalletService[deposit_wallet].collect_deposit!(deposit) } + + let(:options) { {} } + let(:request_method) { :post } + let(:request_path) { '/tbtc/wallet/' + deposit_wallet.bitgo_wallet_id + '/tx/build' } + let(:request_body) {{recipients:[{address: hot_wallet.address, amount: "#{wallet_client.convert_to_base_unit!(deposit.amount)}" }]} } + let(:response_body) {'{"feeInfo": {"fee": 3037}}'} + + let(:set_tx_request_path) { '/tbtc/wallet/' + deposit_wallet.bitgo_wallet_id + '/sendcoins' } + let(:set_tx_request_body) do + { address: hot_wallet.address, + amount: "#{(wallet_client.convert_to_base_unit!(deposit.amount)-3037).to_i}", + walletPassphrase: deposit_wallet.bitgo_wallet_passphrase + } + end + let(:set_tx_response_body) {'{"txid": "dcedf50780f251c99e748362c1a035f2916efb9bb44fe5c5c3e857ea74ca06b3" }'} + + before do + # stub build_raw_transaction request + stub_request(request_method, deposit_wallet.bitgo_rest_api_root + request_path) + .with(body: request_body, headers: request_headers(deposit_wallet)) + .to_return(status: 200, body: response_body, headers: response_headers) + + # stub create_withdrawal request + stub_request(request_method, deposit_wallet.bitgo_rest_api_root + set_tx_request_path) + .with(body: set_tx_request_body, headers: request_headers(deposit_wallet)) + .to_return(status: 200, body: set_tx_response_body, headers: response_headers) + end + + it { is_expected.to eq('dcedf50780f251c99e748362c1a035f2916efb9bb44fe5c5c3e857ea74ca06b3') } + end + + describe '#build_withdrawal!' do + subject { WalletService[hot_wallet].build_withdrawal!(withdraw) } + + let(:options) { {} } + let(:request_method) { :post } + let(:request_path) { '/tbtc/wallet/' + hot_wallet.bitgo_wallet_id + '/sendcoins' } + let(:request_body) do + { address: withdraw.rid, + amount: "#{(wallet_client.convert_to_base_unit!(withdraw.amount)).to_i}", + walletPassphrase: hot_wallet.bitgo_wallet_passphrase + } + end + let(:response_body) {'{"txid": "dcedf50780f251c99e748362c1a035f2916efb9bb44fe5c5c3e857ea74ca06b3" }'} + + before do + stub_request(request_method, hot_wallet.bitgo_rest_api_root + request_path) + .with(body: request_body, headers: request_headers(hot_wallet)) + .to_return(status: 200, body: response_body, headers: response_headers) + end + + it { is_expected.to eq('dcedf50780f251c99e748362c1a035f2916efb9bb44fe5c5c3e857ea74ca06b3') } + end +end diff --git a/spec/services/wallet_service/dashd_spec.rb b/spec/services/wallet_service/dashd_spec.rb new file mode 100644 index 0000000000..0784429350 --- /dev/null +++ b/spec/services/wallet_service/dashd_spec.rb @@ -0,0 +1,100 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +describe WalletService::Dashd do + + around do |example| + WebMock.disable_net_connect! + example.run + WebMock.allow_net_connect! + end + + describe 'WalletService::Dashd' do + + let(:deposit) { create(:deposit, :deposit_dash, amount: 10) } + let(:withdraw) { create(:dash_withdraw) } + let(:deposit_wallet) { Wallet.find_by(gateway: :dashd, kind: :deposit) } + let(:hot_wallet) { Wallet.find_by(gateway: :dashd, kind: :hot) } + + context '#create_address' do + subject { WalletService[deposit_wallet].create_address } + + let(:new_adrress) { 'yborj44WhothaX6vwoMhRMjkq1xELhAWQp' } + + let :getnewaddress_request do + { jsonrpc: '1.0', + method: 'getnewaddress', + params: [] + }.to_json + end + + let :getnewaddress_response do + { result: new_adrress }.to_json + end + + before do + stub_request(:post, deposit_wallet.uri).with(body: getnewaddress_request).to_return(body: getnewaddress_response) + end + + it { is_expected.to eq(address: new_adrress) } + end + + context '#collect_deposit!' do + subject { WalletService[deposit_wallet].collect_deposit!(deposit) } + + let(:txid) { 'dcedf50780f251c99e748362c1a035f2916efb9bb44fe5c5c3e857ea74ca06b3' } + let :listunspent_request do + { + jsonrpc: '1.0', + method: 'listunspent', + params: [1, 10000000, ['yborj44WhothaX6vwoMhRMjkq1xELhAWQp']], + }.to_json + end + + let :listunspent_response do + { result: '0' }.to_json + end + + let :sendtoaddress_request do + { jsonrpc: '1.0', + method: 'sendtoaddress', + params: [hot_wallet.address, deposit.amount, '', '', true] + }.to_json + end + + let :sendtoaddress_response do + { result: txid }.to_json + end + + before do + stub_request(:post, hot_wallet.uri).with(body: listunspent_request).to_return(body: listunspent_response) + stub_request(:post, deposit_wallet.uri).with(body: sendtoaddress_request).to_return(body: sendtoaddress_response) + end + + it { is_expected.to eq([txid]) } + end + + context '#build_withdrawal!' do + subject { WalletService[hot_wallet].build_withdrawal!(withdraw) } + + let(:txid) { 'dcedf50780f251c99e748362c1a035f2916efb9bb44fe5c5c3e857ea74ca06b3' } + + let :sendtoaddress_request do + { jsonrpc: '1.0', + method: 'sendtoaddress', + params: [withdraw.rid, withdraw.amount, '', '', false] + }.to_json + end + + let :sendtoaddress_response do + { result: txid }.to_json + end + + before do + stub_request(:post, hot_wallet.uri).with(body: sendtoaddress_request).to_return(body: sendtoaddress_response) + end + + it { is_expected.to eq(txid) } + end + end +end diff --git a/spec/services/wallet_service/geth_spec.rb b/spec/services/wallet_service/geth_spec.rb new file mode 100644 index 0000000000..a63b10e5dc --- /dev/null +++ b/spec/services/wallet_service/geth_spec.rb @@ -0,0 +1,310 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +describe WalletService::Geth do + + around do |example| + WebMock.disable_net_connect! + example.run + WebMock.allow_net_connect! + end + + let(:deposit_wallet) { Wallet.find_by(currency: :eth, kind: :deposit) } + let(:hot_wallet) { Wallet.find_by(currency: :eth, kind: :hot) } + let(:warm_wallet) { Wallet.find_by(currency: :eth, kind: :warm) } + let(:fee_wallet) { Wallet.find_by(currency: 'eth', kind: 'fee') } + let(:eth_options) { { gas_limit: 21_000, gas_price: 1_000_000_000 } } + + describe '#create_address' do + subject { WalletService[deposit_wallet].create_address } + + let :personal_newAccount_request do + { jsonrpc: '2.0', + id: 1, + method: 'personal_newAccount', + params: %w[ pass@word ] + }.to_json + end + + let :personal_newAccount_response do + { jsonrpc: '2.0', + id: 1, + result: '0x42eb768f2244c8811c63729a21a3569731535f06' + }.to_json + end + + before do + Passgen.stubs(:generate).returns('pass@word') + stub_request(:post, deposit_wallet.uri ).with(body: personal_newAccount_request).to_return(body: personal_newAccount_response) + end + + it { is_expected.to eq(address: '0x42eb768f2244c8811c63729a21a3569731535f06', secret: 'pass@word') } + end + + describe '#collect_deposit!' do + + let(:deposit) { create(:deposit, :deposit_eth, amount: 10) } + let(:eth_payment_address) { deposit.account.payment_address } + let(:issuer) { { address: eth_payment_address.address.downcase, secret: eth_payment_address.secret } } + + let!(:payment_address) do + create(:eth_payment_address, {account: deposit.account, address: '0xe3cb6897d83691a8eb8458140a1941ce1d6e6daa'}) + end + + context 'Collect eth deposit to hot wallet' do + + let(:deposit_wallet_address) { deposit_wallet.address.downcase } + let(:hot_wallet_address) { hot_wallet.address.downcase } + let(:txid) { '0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b' } + + let :eth_getBalance_request do + { jsonrpc: '2.0', + id: 1, + method: 'eth_getBalance', + params: [ hot_wallet_address, 'latest' ], + }.to_json + end + + let :eth_getBalance_response do + { result: '0' }.to_json + end + + let :eth_sendTransaction_request do + { jsonrpc: '2.0', + id: 1, + method: 'eth_sendTransaction', + params: + [ + { + from: issuer[:address], + to: hot_wallet_address, + value: '0x' + (deposit.amount_to_base_unit! - eth_options[:gas_limit] * eth_options[:gas_price]).to_s(16), + gas: '0x' + eth_options[:gas_limit].to_s(16), + gasPrice: '0x' + eth_options[:gas_price].to_s(16) + } + ] + }.to_json + end + + let :eth_sendTransaction_response do + { jsonrpc: '2.0', + id: 2, + result: txid + }.to_json + end + + subject { WalletService[deposit_wallet].collect_deposit!(deposit) } + + before do + stub_request(:post, hot_wallet.uri).with(body: eth_getBalance_request).to_return(body: eth_getBalance_response) + WalletClient[deposit_wallet].class.any_instance.expects(:permit_transaction) + stub_request(:post, deposit_wallet.uri).with(body: eth_sendTransaction_request).to_return(body: eth_sendTransaction_response) + end + + it do + #Transaction to Hot wallet with all deposit amount + is_expected.to eq([txid]) + end + end + + context 'Collect TRST deposit to hot wallet' do + let(:deposit) { create(:deposit, :deposit_trst, amount: 10) } + let(:trst_payment_address) { deposit.account.payment_address } + + let(:deposit_wallet) { Wallet.find_by(currency: :trst, kind: :deposit) } + let(:hot_wallet) { Wallet.find_by(currency: :trst, kind: :hot) } + + let(:issuer) { { address: trst_payment_address.address, secret: trst_payment_address.secret } } + let(:recipient) { { address: hot_wallet.address } } + + let!(:payment_address) do + create(:trst_payment_address, {account: deposit.account, address: '0xe3cb6897d83691a8eb8458140a1941ce1d6e6daa'}) + end + + let(:txid) { '0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b' } + + let :eth_call_request do + { + "jsonrpc":"2.0", + "id":1, + "method":"eth_call", + "params": + [ + { + "to":"0x87099add3bcc0821b5b151307c147215f839a110", + "data":"0x70a08231000000000000000000000000b6a61c43dae37c0890936d720dc42b5cbda990f9" + }, + "latest" + ] + }.to_json + end + + let :eth_call_response do + { result: '0' }.to_json + end + + let :eth_sendTransaction_request do + { jsonrpc: '2.0', + id: 1, + method: 'eth_sendTransaction', + params: + [ + { + from: issuer[:address], + to: '0x87099add3bcc0821b5b151307c147215f839a110', + data: '0xa9059cbb000000000000000000000000b6a61c43dae37c0890936d720dc42b5cbda990f90000000000000000000000000000000000000000000000000000000000989680' + } + ] + }.to_json + end + + let :eth_sendTransaction_response do + { jsonrpc: '2.0', + id: 1, + result: txid + }.to_json + end + + subject { WalletService[deposit_wallet].collect_deposit!(deposit) } + + before do + stub_request(:post, hot_wallet.uri).with(body: eth_call_request).to_return(body: eth_call_response) + WalletClient[deposit_wallet].class.any_instance.expects(:permit_transaction) + stub_request(:post, deposit_wallet.uri).with(body: eth_sendTransaction_request).to_return(body: eth_sendTransaction_response) + end + + it do + is_expected.to eq([txid]) + end + end + end + + describe 'create_withdrawal!' do + let(:issuer) { { address: hot_wallet.address.downcase, secret: hot_wallet.secret } } + let(:recipient) { { address: withdraw.rid.downcase } } + + context 'ETH Withdrawal' do + let(:withdraw) { create(:eth_withdraw, rid: '0x85h43d8a49eeb85d32cf465507dd71d507100c1') } + + let(:txid) { '0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b' } + + let :eth_sendTransaction_request do + { jsonrpc: '2.0', + id: 1, + method: 'eth_sendTransaction', + params: + [ + { + from: issuer[:address], + to: recipient[:address], + value: '0x8a6e51a672858000' + } + ] + }.to_json + end + + let :eth_sendTransaction_response do + { jsonrpc: '2.0', + id: 1, + result: txid + }.to_json + end + + subject { WalletService[hot_wallet].build_withdrawal!(withdraw)} + + before do + WalletClient[hot_wallet].class.any_instance.expects(:permit_transaction) + stub_request(:post, 'http://127.0.0.1:8545/').with(body: eth_sendTransaction_request).to_return(body: eth_sendTransaction_response) + end + + it { is_expected.to eq(txid) } + end + + context 'TRST Withdrawal' do + let(:withdraw) { create(:trst_withdraw, rid: '0x85h43d8a49eeb85d32cf465507dd71d507100c1') } + let(:hot_wallet) { Wallet.find_by(currency: :trst, kind: :hot) } + let(:txid) { '0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b' } + + let :eth_sendTransaction_request do + { jsonrpc: '2.0', + id: 1, + method: 'eth_sendTransaction', + params: + [ + { + from: issuer[:address].downcase, + to: '0x87099add3bcc0821b5b151307c147215f839a110', + data: '0xa9059cbb000000000000000000000000085h43d8a49eeb85d32cf465507dd71d507100c100000000000000000000000000000000000000000000000000000000009834d8' + + } + ] + }.to_json + end + + let :eth_sendTransaction_response do + { jsonrpc: '2.0', + id: 1, + result: txid + }.to_json + end + + subject { WalletService[hot_wallet].build_withdrawal!(withdraw)} + + before do + WalletClient[hot_wallet].class.any_instance.expects(:permit_transaction) + stub_request(:post, 'http://127.0.0.1:8545/').with(body: eth_sendTransaction_request).to_return(body: eth_sendTransaction_response) + end + + it { is_expected.to eq(txid) } + end + end + + describe 'deposit_collection_fees!' do + let(:deposit) { create(:deposit, :deposit_trst, amount: 10) } + let(:trst_payment_address) { deposit.account.payment_address } + + let(:issuer) { { address: fee_wallet.address.downcase, secret: fee_wallet.secret } } + let(:recipient) { { address: trst_payment_address.address.downcase } } + + let!(:payment_address) do + create(:trst_payment_address, {account: deposit.account, address: '0xe3cb6897d83691a8eb8458140a1941ce1d6e6daa'}) + end + + let(:txid) { '0xc6ef2fc5426d6ad6fd9e2a26abeab0aa2411b7ab17f30a99d3cb96aed1d1055b' } + + let :eth_sendTransaction_request do + { jsonrpc: '2.0', + id: 1, + method: 'eth_sendTransaction', + params: + [ + { + from: issuer[:address], + to: recipient[:address], + value: '0x5af3107a4000', + gas: '0x' + eth_options[:gas_limit].to_s(16), + gasPrice: '0x' + eth_options[:gas_price].to_s(16) + } + ] + }.to_json + end + + let :eth_sendTransaction_response do + { jsonrpc: '2.0', + id: 1, + result: txid + }.to_json + end + + subject { WalletService[deposit_wallet].deposit_collection_fees(deposit) } + + before do + WalletClient[deposit_wallet].class.any_instance.expects(:permit_transaction) + stub_request(:post, deposit_wallet.uri).with(body: eth_sendTransaction_request).to_return(body: eth_sendTransaction_response) + end + + it do + is_expected.to eq(txid) + end + end +end diff --git a/spec/services/wallet_service/litecoind_spec.rb b/spec/services/wallet_service/litecoind_spec.rb new file mode 100644 index 0000000000..af1473ed59 --- /dev/null +++ b/spec/services/wallet_service/litecoind_spec.rb @@ -0,0 +1,101 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +describe WalletService::Litecoind do + + around do |example| + WebMock.disable_net_connect! + example.run + WebMock.allow_net_connect! + end + + describe 'WalletService::Litecoind' do + + let(:deposit) { create(:deposit, :deposit_ltc, amount: 10) } + let(:withdraw) { create(:ltc_withdraw) } + let(:deposit_wallet) { Wallet.find_by(gateway: :litecoind, kind: :deposit) } + let(:hot_wallet) { Wallet.find_by(gateway: :litecoind, kind: :hot) } + + context '#create_address' do + subject { WalletService[deposit_wallet].create_address } + + let(:new_adrress) { 'QcM2zjgbaXbH26utxnNFge24A1BnDgSgcU' } + + let :getnewaddress_request do + { jsonrpc: '1.0', + method: 'getnewaddress', + params: [] + }.to_json + end + + let :getnewaddress_response do + { result: new_adrress }.to_json + end + + before do + stub_request(:post, deposit_wallet.uri).with(body: getnewaddress_request).to_return(body: getnewaddress_response) + end + + it { is_expected.to eq(address: new_adrress) } + end + + context '#collect_deposit!' do + subject { WalletService[deposit_wallet].collect_deposit!(deposit) } + + let(:txid) { 'dcedf50780f251c99e748362c1a035f2916efb9bb44fe5c5c3e857ea74ca06b3' } + + let :listunspent_request do + { + jsonrpc: '1.0', + method: 'listunspent', + params: [1, 10000000, ['Qc2BM7gp8mKgJPPxLAadLAHteNQwhFwwuf']], + }.to_json + end + + let :listunspent_response do + { result: '0' }.to_json + end + + let :sendtoaddress_request do + { jsonrpc: '1.0', + method: 'sendtoaddress', + params: [hot_wallet.address, deposit.amount, '', '', true] + }.to_json + end + + let :sendtoaddress_response do + { result: txid }.to_json + end + + before do + stub_request(:post, hot_wallet.uri).with(body: listunspent_request).to_return(body: listunspent_response) + stub_request(:post, deposit_wallet.uri).with(body: sendtoaddress_request).to_return(body: sendtoaddress_response) + end + + it { is_expected.to eq([txid]) } + end + + context '#build_withdrawal!' do + subject { WalletService[hot_wallet].build_withdrawal!(withdraw) } + + let(:txid) { 'dcedf50780f251c99e748362c1a035f2916efb9bb44fe5c5c3e857ea74ca06b3' } + + let :sendtoaddress_request do + { jsonrpc: '1.0', + method: 'sendtoaddress', + params: [withdraw.rid, withdraw.amount, '', '', false] + }.to_json + end + + let :sendtoaddress_response do + { result: 'dcedf50780f251c99e748362c1a035f2916efb9bb44fe5c5c3e857ea74ca06b3' }.to_json + end + + before do + stub_request(:post, hot_wallet.uri).with(body: sendtoaddress_request).to_return(body: sendtoaddress_response) + end + + it { is_expected.to eq(txid) } + end + end +end diff --git a/spec/services/wallet_service/rippled_spec.rb b/spec/services/wallet_service/rippled_spec.rb index 803b56a474..d94cbc01da 100644 --- a/spec/services/wallet_service/rippled_spec.rb +++ b/spec/services/wallet_service/rippled_spec.rb @@ -1,19 +1,185 @@ +# encoding: UTF-8 # frozen_string_literal: true -describe 'WalletService::Ripple' do - describe '#create_address!' do - let(:service) { WalletService[wallet] } - let(:wallet) { Wallet.find_by_blockchain_key('xrp-testnet') } - let(:create_address) { service.create_address } +describe WalletService::Rippled do - it 'create valid address with destination_tag' do - address = create_address[:address] - expect(normalize_address(address)).to eq wallet.address - expect(destination_tag_from(address).to_i).to be > 0 + around do |example| + WebMock.disable_net_connect! + example.run + WebMock.allow_net_connect! + end + + describe 'WalletService::Rippled' do + + let(:sign_data) do + Rails.root.join('spec', 'resources', 'ripple-data', 'sign-transaction.json') + .yield_self {|file_path| File.open(file_path)} + .yield_self {|file| JSON.load(file)} + .to_json + end + + let(:submit_data) do + Rails.root.join('spec', 'resources', 'ripple-data', 'submit-transaction.json') + .yield_self {|file_path| File.open(file_path)} + .yield_self {|file| JSON.load(file)} + .to_json + end + + let(:deposit) {create(:deposit, :deposit_xrp, address: 'rN3J1yMz2PCGievtS2XTEgkrmdHiJgzb5Y', amount: 10)} + let(:withdraw) {create(:xrp_withdraw)} + let(:deposit_wallet) { Wallet.find_by(gateway: :rippled, kind: :deposit)} + let(:hot_wallet) { Wallet.find_by(gateway: :rippled, kind: :hot)} + + context '#create_address!' do + let(:service) {WalletService[wallet]} + let(:wallet) {Wallet.find_by(gateway: :rippled, kind: :deposit)} + let(:create_address) {service.create_address} + + it 'create valid address with destination_tag' do + address = create_address[:address] + expect(normalize_address(address)).to eq wallet.address + expect(destination_tag_from(address).to_i).to be > 0 + end + end + + context '#collect_deposit' do + + subject { WalletService[deposit_wallet].collect_deposit!(deposit) } + + let!(:payment_address) do + create(:xrp_payment_address, {account: deposit.account, address: 'rN3J1yMz2PCGievtS2XTEgkrmdHiJgzb5Y?dt=917590223', secret: 'changeme'}) + end + + let :account_info_response do + { result: '0' }.to_json + end + + let :account_info_request do + { + jsonrpc: '1.0', + id: 1, + method: 'account_info', + params: + [ + { + account: hot_wallet.address, + ledger_index: 'validated', + strict: true + } + ] + }.to_json + end + + let :sign_request do + {jsonrpc: '1.0', + id: 1, + method: 'sign', + params: + [ + secret: 'changeme', + tx_json: + { + Account: deposit.address, + Amount: '9990000', + Fee: 10000, + Destination: hot_wallet.address, + DestinationTag: 0, + TransactionType: 'Payment', + LastLedgerSequence: 31234504 + } + ] + }.to_json + end + + let :submit_request do + { + jsonrpc: '1.0', + id: 2, + method: 'submit', + params: + [ + tx_blob: '1200002280000000240000016861D4838D7EA4C6800000000000000000000000000055534400000000004B4E9C06F24296074F7BC48F92A97916C6DC5'\ + 'EA9684000000000002710732103AB40A0490F9B7ED8DF29D246BF2D6269820A0EE7742ACDD457BEA7C7D0931EDB7446304402200E5C2DD81FDF0BE9AB'\ + '2A8D797885ED49E804DBF28E806604D878756410CA98B102203349581946B0DDA06B36B35DBC20EDA27552C1F167BCF5C6ECFF49C6A46F858081144B4'\ + 'E9C06F24296074F7BC48F92A97916C6DC5EA983143E9D4A2B8AA0780F682D136F7A56D6724EF53754' + ] + }.to_json + end + + subject { WalletService[deposit_wallet].collect_deposit!(deposit) } + + before do + stub_request(:post, hot_wallet.uri).with(body: account_info_request).to_return(body: account_info_response) + WalletClient[hot_wallet].class.any_instance.expects(:calculate_current_fee).returns(10000) + WalletClient[hot_wallet].class.any_instance.expects(:latest_block_number).returns(31234500) + stub_request(:post, deposit_wallet.uri).with(body: sign_request).to_return(body: sign_data) + stub_request(:post, deposit_wallet.uri).with(body: submit_request).to_return(body: submit_data) + end + + it do + is_expected.to eq(["5B31A7518DC304D5327B4887CD1F7DC2C38D5F684170097020C7C9758B973847"]) + end + end + + context '#build_withdrawal!' do + + let(:withdraw) { create(:xrp_withdraw, rid: 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn') } + + let :sign_request do + { jsonrpc: '1.0', + id: 1, + method: 'sign', + params: + [ + secret: 'changeme', + tx_json: + { + Account: hot_wallet.address, + Amount: '9975000', + Fee: 10000, + Destination: withdraw.rid, + DestinationTag: 0, + TransactionType: 'Payment', + LastLedgerSequence: 31234504 + } + ] + }.to_json + end + + let :submit_request do + { + jsonrpc: '1.0', + id: 2, + method: 'submit', + params: + [ + tx_blob: '1200002280000000240000016861D4838D7EA4C6800000000000000000000000000055534400000000004B4E9C06F24296074F7BC48F92A97916C6DC5'\ + 'EA9684000000000002710732103AB40A0490F9B7ED8DF29D246BF2D6269820A0EE7742ACDD457BEA7C7D0931EDB7446304402200E5C2DD81FDF0BE9AB'\ + '2A8D797885ED49E804DBF28E806604D878756410CA98B102203349581946B0DDA06B36B35DBC20EDA27552C1F167BCF5C6ECFF49C6A46F858081144B4'\ + 'E9C06F24296074F7BC48F92A97916C6DC5EA983143E9D4A2B8AA0780F682D136F7A56D6724EF53754' + ] + }.to_json + end + + subject { WalletService[hot_wallet].build_withdrawal!(withdraw) } + + before do + WalletClient[hot_wallet].class.any_instance.expects(:calculate_current_fee).returns(10000) + WalletClient[hot_wallet].class.any_instance.expects(:latest_block_number).returns(31234500) + # Request with method 'sign' to return a signed binary representation of the transaction. + stub_request(:post, deposit_wallet.uri).with(body: sign_request).to_return(body: sign_data) + # Request with method 'submit' method to apply a transaction and send it to the network to be confirmed and included in future ledgers + stub_request(:post, deposit_wallet.uri).with(body: submit_request).to_return(body: submit_data) + end + + it do + is_expected.to eq('5B31A7518DC304D5327B4887CD1F7DC2C38D5F684170097020C7C9758B973847') + end end end end + def normalize_address(address) address.gsub(/\?dt=\d*\Z/, '') end diff --git a/spec/services/wallet_service_spec.rb b/spec/services/wallet_service_spec.rb new file mode 100644 index 0000000000..ee5df7519c --- /dev/null +++ b/spec/services/wallet_service_spec.rb @@ -0,0 +1,230 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +describe WalletService do + + around do |example| + WebMock.disable_net_connect! + example.run + WebMock.allow_net_connect! + end + + let(:deposit_wallet) { Wallet.find_by(currency: :eth, kind: :deposit) } + let(:hot_wallet) { Wallet.find_by(currency: :eth, kind: :hot) } + let(:warm_wallet) { Wallet.find_by(currency: :eth, kind: :warm) } + + describe 'spread deposit' do + + context 'Deposit divided in two wallets (hot and warm)' do + + let(:deposit) { create(:deposit, :deposit_eth, amount: 100) } + + let :hot_wallet_eth_getBalance_response do + { result: '2b5e3af16b1880000' }.to_json + end + + let :warm_wallet_eth_getBalance_response do + { result: '0' }.to_json + end + + let :hot_wallet_eth_getBalance_request do + { + jsonrpc: '2.0', + id: 1, + method: 'eth_getBalance', + params: [ hot_wallet[:address].downcase, 'latest' ], + }.to_json + end + + let :warm_wallet_eth_getBalance_request do + { + jsonrpc: '2.0', + id: 1, + method: 'eth_getBalance', + params: [ warm_wallet[:address].downcase, 'latest' ], + }.to_json + end + + let :spread_hash do + { + "0xb6a61c43DAe37c0890936D720DC42b5CBda990F9"=>0.5e2, + "0x2b9fBC10EbAeEc28a8Fc10069C0BC29E45eBEB9C"=>0.5e2 + } + end + + subject { WalletService[deposit_wallet].send(:spread_deposit, deposit) } + + before do + # Hot wallet balance = 50 eth + stub_request(:post, hot_wallet.uri).with(body: hot_wallet_eth_getBalance_request).to_return(body: hot_wallet_eth_getBalance_response) + # Warm wallet balance = 0 eth + stub_request(:post, hot_wallet.uri).with(body: warm_wallet_eth_getBalance_request).to_return(body: warm_wallet_eth_getBalance_response) + end + it do + # Deposit amount 100 eth + # Collect 50 eth to Hot wallet and 50 eth to Warm wallet + is_expected.to eq(spread_hash) + end + end + + context 'Deposit divided in two wallets and collect all remaining to last wallet(warm)' do + + let(:deposit) { create(:deposit, :deposit_eth, amount: 200) } + + let :hot_wallet_eth_getBalance_response do + { result: '2b5e3af16b1880000' }.to_json + end + + let :warm_wallet_eth_getBalance_response do + { result: '0' }.to_json + end + + let :hot_wallet_eth_getBalance_request do + { + jsonrpc: '2.0', + id: 1, + method: 'eth_getBalance', + params: [ hot_wallet[:address].downcase, 'latest' ], + }.to_json + end + + let :warm_wallet_eth_getBalance_request do + { + jsonrpc: '2.0', + id: 1, + method: 'eth_getBalance', + params: [ warm_wallet[:address].downcase, 'latest' ], + }.to_json + end + + let :spread_hash do + { + "0xb6a61c43DAe37c0890936D720DC42b5CBda990F9"=>0.5e2, + "0x2b9fBC10EbAeEc28a8Fc10069C0BC29E45eBEB9C"=>1.5e2 + } + end + + subject { WalletService[deposit_wallet].send(:spread_deposit, deposit) } + + before do + warm_wallet.update!(max_balance: 100) + # Hot wallet balance = 50 eth + stub_request(:post, hot_wallet.uri).with(body: hot_wallet_eth_getBalance_request).to_return(body: hot_wallet_eth_getBalance_response) + # Warm wallet balance = 0 eth + stub_request(:post, hot_wallet.uri).with(body: warm_wallet_eth_getBalance_request).to_return(body: warm_wallet_eth_getBalance_response) + end + it do + # Deposit amount 200 eth + # Collect 50 eth to Hot wallet and 150 eth to Warm wallet(last wallet) + is_expected.to eq(spread_hash) + end + end + + context 'Deposit doesn\'t fit in any wallet' do + let(:deposit) { create(:deposit, :deposit_eth, amount: 200) } + + let :hot_wallet_eth_getBalance_response do + { result: '56bc75e2d63100000' }.to_json + end + + let :warm_wallet_eth_getBalance_response do + { result: '821ab0d4414980000' }.to_json + end + + let :hot_wallet_eth_getBalance_request do + { + jsonrpc: '2.0', + id: 1, + method: 'eth_getBalance', + params: [ hot_wallet[:address].downcase, 'latest' ], + }.to_json + end + + let :warm_wallet_eth_getBalance_request do + { + jsonrpc: '2.0', + id: 1, + method: 'eth_getBalance', + params: [ warm_wallet[:address].downcase, 'latest' ], + }.to_json + end + + let :spread_hash do + { + "0x2b9fBC10EbAeEc28a8Fc10069C0BC29E45eBEB9C"=>2e2 + } + end + + subject { WalletService[deposit_wallet].send(:spread_deposit, deposit) } + + before do + hot_wallet.update!(max_balance: 100) + warm_wallet.update!(max_balance: 150) + # Hot wallet balance = 100 eth + stub_request(:post, hot_wallet.uri).with(body: hot_wallet_eth_getBalance_request).to_return(body: hot_wallet_eth_getBalance_response) + # Warm wallet balance = 150 eth + stub_request(:post, hot_wallet.uri).with(body: warm_wallet_eth_getBalance_request).to_return(body: warm_wallet_eth_getBalance_response) + end + + it do + # Deposit amount 200 eth + # Collect all deposit to last wallet + is_expected.to eq(spread_hash) + end + end + + context 'Intermediate amount is less than min collection amount in hot wallet' do + let(:deposit) { create(:deposit, :deposit_eth, amount: 100) } + + let :hot_wallet_eth_getBalance_response do + { result: '35ab028ac154b80000' }.to_json + end + + let :warm_wallet_eth_getBalance_response do + { result: '0' }.to_json + end + + let :hot_wallet_eth_getBalance_request do + { + jsonrpc: '2.0', + id: 1, + method: 'eth_getBalance', + params: [ hot_wallet[:address].downcase, 'latest' ], + }.to_json + end + + let :warm_wallet_eth_getBalance_request do + { + jsonrpc: '2.0', + id: 1, + method: 'eth_getBalance', + params: [ warm_wallet[:address].downcase, 'latest' ], + }.to_json + end + + let :spread_hash do + { + "0x2b9fBC10EbAeEc28a8Fc10069C0BC29E45eBEB9C"=>0.1e3 + } + end + + subject { WalletService[deposit_wallet].send(:spread_deposit, deposit) } + + before do + hot_wallet.update!(max_balance: 100) + warm_wallet.update!(max_balance: 200) + deposit.currency.update!(min_deposit_amount: 2) + # Hot wallet balance = 99 eth + stub_request(:post, hot_wallet.uri).with(body: hot_wallet_eth_getBalance_request).to_return(body: hot_wallet_eth_getBalance_response) + # Warm wallet balance = 0 eth + stub_request(:post, hot_wallet.uri).with(body: warm_wallet_eth_getBalance_request).to_return(body: warm_wallet_eth_getBalance_response) + end + + it do + # Deposit amount 100 eth + # Collect all deposit to warm wallet + is_expected.to eq(spread_hash) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 18c59f8eb1..5ce1daa3d2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -90,8 +90,9 @@ I18n.locale = :en %w[ eth-rinkeby btc-testnet dash-testnet ltc-testnet bch-testnet xrp-testnet ].each { |blockchain| FactoryBot.create(:blockchain, blockchain) } %i[ usd btc dash eth xrp trst bch eur ltc ].each { |ccy| FactoryBot.create(:currency, ccy) } - %i[ eth_hot btc_hot btc_deposit xrp_hot].each { |ccy| FactoryBot.create(:wallet, ccy) } - %i[ btcusd dashbtc btceth btcxrp].each { |market| FactoryBot.create(:market, market) } + %i[ eth_deposit eth_hot eth_fee trst_deposit trst_hot btc_hot btc_deposit bch_deposit bch_hot dash_deposit dash_hot ltc_deposit ltc_hot xrp_deposit xrp_hot eth_warm ] + .each { |ccy| FactoryBot.create(:wallet, ccy) } + %i[ btcusd dashbtc btceth btcxrp ].each { |market| FactoryBot.create(:market, market) } end config.append_after :each do