module Peatio module Coinpaymentnew class Wallet < Peatio::Wallet::Abstract TIME_DIFFERENCE_IN_MINUTES = 10 # XLM_MEMO_TYPES = { 'memoId': 'id', 'memoText': 'text', 'memoHash': 'hash', 'memoReturn': 'return' } DEFAULT_FEATURES = { skip_deposit_collection: false }.freeze def initialize(custom_features = {}) @features = DEFAULT_FEATURES.merge(custom_features).slice(*SUPPORTED_FEATURES) @settings = {} end def configure(settings = {}) # Clean client state during configure. @client = nil @settings.merge!(settings.slice(*SUPPORTED_SETTINGS)) @wallet = @settings.fetch(:wallet) do raise Peatio::Wallet::MissingSettingError, :wallet end.slice(:uri, :address, :secret, :access_token, :wallet_id, :testnet) @currency = @settings.fetch(:currency) do raise Peatio::Wallet::MissingSettingError, :currency end.slice(:id, :base_factor, :code, :options) end def create_address!(options = {}) currency = erc20_currency_id options.deep_symbolize_keys! response = WalletApi.get_deposit_address(currency.upcase) { address: response[:address], secret: coinpayment_wallet_passphrase, details: { address_id: response[:address] }} # if options.dig(:pa_details, :address_id).present? && # options.dig(:pa_details, :updated_at).present? && # time_difference_in_minutes(options.dig(:pa_details, :updated_at)) >= TIME_DIFFERENCE_IN_MINUTES # # response = client.rest_api(:get, "#{currency}/wallet/#{wallet_id}/address/#{options.dig(:pa_details, :address_id)}") # { address: response['address'], secret: bitgo_wallet_passphrase } # elsif options.dig(:pa_details, :address_id).blank? # response = client.rest_api(:post, "#{currency}/wallet/#{wallet_id}/address") # { address: response['address'], secret: bitgo_wallet_passphrase, details: { address_id: response['id'] }} # end rescue Coinpaymentnew::Client::Error => e raise Peatio::Wallet::ClientError, e end def create_transaction!(transaction, options = {}) currency_options = @currency.fetch(:options).slice(:gas_limit, :gas_price, :erc20_contract_address) # if currency_options[:gas_limit].present? && currency_options[:gas_price].present? # options.merge!(currency_options) # create_eth_transaction(transaction, options) # else amount = convert_to_base_unit(transaction.amount) # if options[:subtract_fee].to_s == 'true' # fee = build_raw_transaction(transaction) # baseFeeInfo = fee.dig('feeInfo','fee') # fee = baseFeeInfo.present? ? baseFeeInfo : fee.dig('txInfo','Fee') # amount -= fee.to_i # end response = WalletApi.createTransaction(transaction.amount.to_s, "USD",currency_id.upcase, transaction.to_address.to_s) # response = client.rest_api(:post, "#{currency_id}/wallet/#{wallet_id}/sendcoins", { # address: normalize_address(transaction.to_address.to_s), # amount: amount.to_s, # walletPassphrase: bitgo_wallet_passphrase, # memo: xlm_memo(transaction.to_address.to_s) # }.compact) # # if response['feeString'].present? # fee = convert_from_base_unit(response['feeString']) # transaction.fee = fee # end transaction.hash = normalize_txid(response[:txn_id]) transaction.fee_currency_id = erc20_currency_id transaction # end rescue Coinpaymentnew::Client::Error => e raise Peatio::Wallet::ClientError, e end # # def build_raw_transaction(transaction) # client.rest_api(:post, "#{currency_id}/wallet/#{wallet_id}/tx/build", { # recipients: [{ # address: transaction.to_address, # amount: convert_to_base_unit(transaction.amount).to_s # }] # }.compact) # end # def create_eth_transaction(transaction, options = {}) # amount = convert_to_base_unit(transaction.amount) # hop = true unless options.slice(:gas_price).present? # # fee_estimate = fee_estimate(amount.to_s, hop) # # if transaction.options.present? && transaction.options[:gas_price].present? # options[:gas_price] = transaction.options[:gas_price] # else # options[:gas_price] = fee_estimate['minGasPrice'].to_i # end # # response = client.rest_api(:post, "#{currency_id}/wallet/#{wallet_id}/sendcoins", { # address: transaction.to_address.to_s, # amount: amount.to_s, # walletPassphrase: bitgo_wallet_passphrase, # gas: options.fetch(:gas_limit).to_i, # gasPrice: options.fetch(:gas_price).to_i, # hop: hop # }.compact) # # if response['feeString'].present? # fee = convert_from_base_unit(response['feeString']) # transaction.fee = fee # end # # transaction.hash = normalize_txid(response['txid']) # transaction.fee_currency_id = erc20_currency_id # transaction.options = options # transaction # end def fee_estimate(amount, hop) client.rest_api(:get, "#{erc20_currency_id}/tx/fee", { amount: amount, hop: hop }.compact) end def load_balance! # if @currency.fetch(:options).slice(:erc20_contract_address).present? # load_erc20_balance! # else # response = client.rest_api(:get, "#{currency_id}/wallet/#{wallet_id}") response = WalletApi.balance response = response.fetch(currency_id) convert_from_base_unit(response["balance"]) # end rescue Coinpaymentnew::Client::Error => e raise Peatio::Wallet::ClientError, e end # def load_erc20_balance! # response = client.rest_api(:get, "#{erc20_currency_id}/wallet/#{wallet_id}?allTokens=true") # convert_from_base_unit(response.dig('tokens', currency_id, 'balanceString')) # rescue Coinpaymentnew::Client::Error => e # raise Peatio::Wallet::ClientError, e # end def trigger_webhook_event(request) currency = @wallet.fetch(:testnet).present? ? 't' + @currency.fetch(:id) : @currency.fetch(:id) if request.params['type'] == 'transfer' return unless currency == request.params['coin'] && @wallet.fetch(:wallet_id) == request.params['wallet'] else return unless @wallet.fetch(:wallet_id) == request.params['walletId'] end if request.params['type'] == 'transfer' transactions = fetch_transfer!(request.params['transfer']) return transactions elsif request.params['type'] == 'address_confirmation' address_id = fetch_address_id(request.params['address']) return { address_id: address_id, address: request.params['address'], currency_id: currency_id } end end def register_webhooks!(url) transfer_webhook(url) address_confirmation_webhook(url) end # def fetch_address_id(address) # currency = erc20_currency_id # client.rest_api(:get, "#{currency}/wallet/#{wallet_id}/address/#{address}") # .fetch('id') # rescue Coinpaymentnew::Client::Error => e # raise Peatio::Wallet::ClientError, e # end def fetch_transfer!(id) response = client.rest_api(:get, "#{currency_id}/wallet/#{wallet_id}/transfer/#{id}") parse_entries(response['entries']).map do |entry| to_address = if response.dig('coinSpecific', 'memo').present? memo = response.dig('coinSpecific', 'memo') memo_type = memo.kind_of?(Array) ? memo.first : memo build_address(entry['address'], memo_type) else entry['address'] end state = define_transaction_state(response['state']) if response['outputs'].present? output = response['outputs'].find { |out| out['address'] == to_address } txout = output['index'] if output.present? end if response['feeString'].present? fee = convert_from_base_unit(response['feeString']) / response['entries'].count end transaction = Peatio::Transaction.new( currency_id: @currency.fetch(:id), amount: convert_from_base_unit(entry['valueString']), fee: fee, fee_currency_id: erc20_currency_id, hash: normalize_txid(response['txid']), to_address: to_address, block_number: response['height'], txout: txout.to_i, status: state ) transaction if transaction.valid? end.compact rescue Coinpaymentnew::Client::Error => e raise Peatio::Wallet::ClientError, e end def transfer_webhook(url) client.rest_api(:post, "#{currency_id}/wallet/#{wallet_id}/webhooks", { type: 'transfer', allToken: true, url: url, label: "webhook for #{url}", listenToFailureStates: false }) end def address_confirmation_webhook(url) client.rest_api(:post, "#{currency_id}/wallet/#{wallet_id}/webhooks", { type: 'address_confirmation', allToken: true, url: url, label: "webhook for #{url}", listenToFailureStates: false }) end def parse_entries(entries) entries.map do |e| e if e["valueString"].to_i.positive? end.compact end private def client uri = @wallet.fetch(:uri) { raise Peatio::Wallet::MissingSettingError, :uri } access_token = @wallet.fetch(:access_token) { raise Peatio::Wallet::MissingSettingError, :access_token } currency_code_prefix = @wallet.fetch(:testnet) ? 't' : '' uri = uri.gsub(/\/+\z/, '') + '/' + currency_code_prefix @client ||= Client.new(uri, access_token) end def build_address(address, memo) "#{address}?memoId=#{memo['value']}" end # All these functions will have to be done with the coin set to eth or teth # since that is the actual coin type being used. def erc20_currency_id return 'eth' if @currency.fetch(:options).slice(:erc20_contract_address).present? currency_id end def xlm_memo(address) build_xlm_memo(address) if @currency.fetch(:id) == 'xlm' end def build_xlm_memo(address) case address.split('?').last.split('=').first when 'memoId' memo_value_from(address, 'memoId') when 'memoText' memo_value_from(address, 'memoText') when 'memoHash' memo_value_from(address, 'memoHash') when 'memoReturn' memo_value_from(address, 'memoReturn') end end def memo_value_from(address, type) memo_value = address.partition(type + '=').last return { type: XLM_MEMO_TYPES[type.to_sym], value: memo_value } if memo_value.present? end def currency_id @currency.fetch(:id) { raise Peatio::Wallet::MissingSettingError, :id } end def coinpayment_wallet_passphrase @wallet.fetch(:secret) end def wallet_id @wallet.fetch(:wallet_id) end def normalize_address(address) if @currency.fetch(:id) == 'xlm' address.split('?').first else address end end def normalize_txid(txid) txid.downcase end def convert_from_base_unit(value) value.to_d / @currency.fetch(:base_factor) end def convert_to_base_unit(value) x = value.to_d * @currency.fetch(:base_factor) unless (x % 1).zero? raise Peatio::WalletClient::Error, "Failed to convert value to base (smallest) unit because it exceeds the maximum precision: " \ "#{value.to_d} - #{x.to_d} must be equal to zero." end x.to_i end def time_difference_in_minutes(updated_at) (Time.now - updated_at)/60 end def define_transaction_state(state) case state when 'unconfirmed' 'pending' when 'confirmed' 'success' when 'failed','rejected' 'failed' end end end end end