coins_paid_api 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 30840fa192fc3b46c33fa213287229f8662f75c03b049a66a70656ec8ce207dd
4
+ data.tar.gz: c7d3e06a33e8dc791e784e28c155e9b8e2f9d624c68b7f2e81f4a48164055b94
5
+ SHA512:
6
+ metadata.gz: e758833c389da026a1ca81e19556bfc2a3d1348987988a0c205da2bb9598afc023afac3f2cfbf7df7dccdfa295ecb323b30402c2e94e850ba43f0f86b4ab2a7a
7
+ data.tar.gz: 5f5561e85f892abb70f6036f3697221f8ded16f2c5a63a6d65a19ba794d1ca6283386f659bf90f2e47ffbba86de2ecfaab60f50cd3b137155ee56e6c3c6e3ce7
@@ -0,0 +1,108 @@
1
+ version: 2.0
2
+ defaults: &defaults
3
+ docker:
4
+ - image: circleci/ruby:2.5-stretch-node
5
+ working_directory: ~/coins_paid_api
6
+
7
+ jobs:
8
+ checkout_code:
9
+ <<: *defaults
10
+ steps:
11
+ - checkout
12
+ - run: mkdir log
13
+ - save_cache:
14
+ key: v1-repo-{{ .Environment.CIRCLE_SHA1 }}
15
+ paths:
16
+ - ~/coins_paid_api
17
+
18
+ download_cc_reporter:
19
+ <<: *defaults
20
+ steps:
21
+ - run:
22
+ name: Download cc-test-reporter
23
+ command: |
24
+ mkdir -p cc/
25
+ curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc/cc-test-reporter
26
+ chmod +x ./cc/cc-test-reporter
27
+ - persist_to_workspace:
28
+ root: ~/coins_paid_api
29
+ paths:
30
+ - cc/cc-test-reporter
31
+
32
+ run_bundler:
33
+ <<: *defaults
34
+ steps:
35
+ - restore_cache:
36
+ key: v1-repo-{{ .Environment.CIRCLE_SHA1 }}
37
+ - restore_cache:
38
+ key: v1-bundle-{{ checksum "Gemfile" }}
39
+ - run: echo "export rvm_ignore_gemsets_flag=1" >> ~/.rvmrc
40
+ - run: sudo apt-get update && sudo apt-get install -y libsodium-dev
41
+ - run: bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3
42
+ - save_cache:
43
+ key: v1-bundle-{{ checksum "Gemfile" }}
44
+ paths:
45
+ - ~/coins_paid_api/vendor/bundle
46
+
47
+ run_rspec_tests:
48
+ <<: *defaults
49
+ docker:
50
+ - image: circleci/ruby:2.5-stretch-node
51
+ environment:
52
+ RACK_ENV: "test"
53
+ CIRCLE_ARTIFACTS: "./tmp"
54
+ steps:
55
+ - restore_cache:
56
+ key: v1-repo-{{ .Environment.CIRCLE_SHA1 }}
57
+ - restore_cache:
58
+ key: v1-bundle-{{ checksum "Gemfile" }}
59
+ - attach_workspace:
60
+ at: ~/coins_paid_api
61
+ - run: sudo apt-get update && sudo apt-get install -y libsodium-dev
62
+ - run: bundle check --path vendor/bundle
63
+ - run: bundle exec rspec spec
64
+ - run: ./cc/cc-test-reporter format-coverage -t simplecov -o cc/codeclimate.rspec.json tmp/coverage/.resultset.json
65
+ - persist_to_workspace:
66
+ root: ~/coins_paid_api
67
+ paths:
68
+ - cc/codeclimate.rspec.json
69
+
70
+ upload_cc_coverage:
71
+ <<: *defaults
72
+ steps:
73
+ - attach_workspace:
74
+ at: ~/coins_paid_api
75
+ - run:
76
+ name: Upload coverage results to Code Climate
77
+ command: |
78
+ ./cc/cc-test-reporter upload-coverage -i cc/codeclimate.rspec.json
79
+
80
+ deploy:
81
+ <<: *defaults
82
+ steps:
83
+ - restore_cache:
84
+ key: v1-repo-{{ .Environment.CIRCLE_SHA1 }}
85
+ - restore_cache:
86
+ key: v1-bundle-{{ checksum "Gemfile" }}
87
+ - run: mkdir -p ~/.ssh && ssh-keyscan -H github.com >> ~/.ssh/known_hosts
88
+ - run: sudo apt-get update && sudo apt-get install -y libsodium-dev
89
+ - run: bundle check --path vendor/bundle
90
+ - run: bundle exec rake deploy:branch_or_label
91
+
92
+ workflows:
93
+ version: 2
94
+ build_test_deploy:
95
+ jobs:
96
+ - checkout_code
97
+ - download_cc_reporter:
98
+ requires:
99
+ - checkout_code
100
+ - run_bundler:
101
+ requires:
102
+ - download_cc_reporter
103
+ - run_rspec_tests:
104
+ requires:
105
+ - run_bundler
106
+ - upload_cc_coverage:
107
+ requires:
108
+ - run_rspec_tests
@@ -0,0 +1,2 @@
1
+ /*.gem
2
+ /Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source "http://www.rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :development, :test do
6
+ gem 'pry'
7
+ end
8
+
9
+ group :test do
10
+ gem 'simplecov', require: false
11
+ end
@@ -0,0 +1,15 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'coins_paid_api'
3
+ s.authors = ['Artem Biserov(artembiserov)', 'Oleg Ivanov(morhekil)']
4
+ s.version = '1.0.1'
5
+ s.files = `git ls-files`.split("\n")
6
+ s.summary = 'Coins Paid Integration'
7
+ s.license = 'Nonstandard'
8
+
9
+ s.add_runtime_dependency 'dry-initializer', '~> 3.0'
10
+ s.add_runtime_dependency 'dry-struct', '~> 1.0'
11
+ s.add_runtime_dependency 'faraday', '~> 0.12'
12
+ s.add_runtime_dependency 'faraday_middleware', '~> 0.11'
13
+ s.add_development_dependency 'rspec', '~> 3.0'
14
+ s.add_development_dependency 'webmock', '~> 3.7'
15
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+ require 'dry-struct'
3
+ require 'json'
4
+ require 'faraday'
5
+ require 'faraday_middleware'
6
+ require_relative 'api/types'
7
+ require_relative 'api/signature'
8
+ require_relative 'api/requester'
9
+ require_relative 'api/transport'
10
+ require_relative 'api/callback_data'
11
+ require_relative 'api/currencies_list'
12
+ require_relative 'api/take_address'
13
+ require_relative 'api/withdrawal'
14
+
15
+ module CoinsPaid
16
+ module API
17
+ module_function
18
+
19
+ Error = Class.new RuntimeError
20
+ ProcessingError = Class.new Error
21
+ ConnectionError = Class.new Error
22
+ InvalidSignatureError = Class.new Error
23
+
24
+ URL = 'https://app.coinspaid.com/api/v2/'
25
+
26
+ class << self
27
+ attr_accessor :public_key
28
+ attr_accessor :secret_key
29
+ end
30
+
31
+ @public_key = ENV['COINS_PAID_PUBLIC_KEY']
32
+ @secret_key = ENV['COINS_PAID_SECRET_KEY']
33
+
34
+ def configure
35
+ yield self
36
+ end
37
+
38
+ def take_address(foreign_id:, currency:, convert_to:)
39
+ Requester.call(
40
+ TakeAddress,
41
+ foreign_id: foreign_id, currency: currency, convert_to: convert_to
42
+ )
43
+ end
44
+
45
+ def withdraw(foreign_id:, amount:, currency:, convert_to:, address:)
46
+ Requester.call(
47
+ Withdrawal,
48
+ foreign_id: foreign_id, amount: amount, currency: currency, convert_to: convert_to, address: address
49
+ )
50
+ end
51
+
52
+ def currencies_list
53
+ CurrenciesList.call
54
+ end
55
+
56
+ def callback(request_body, headers)
57
+ Signature.check!(
58
+ request_body: request_body,
59
+ key: headers['X-Processing-Key'],
60
+ signature: headers['X-Processing-Signature']
61
+ )
62
+
63
+ CallbackData.from_json(JSON.parse(request_body, symbolize_names: true))
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CoinsPaid
4
+ module API
5
+ class CallbackData < Dry::Struct
6
+ NOT_CONFIRMED = 'not_confirmed'
7
+ CANCELLED = 'cancelled'
8
+ TNX_TYPE_BLOCKCHAIN = 'blockchain'
9
+
10
+ attribute :id, Types::Integer
11
+ attribute :foreign_id, Types::String
12
+ attribute? :type, Types::String
13
+ attribute? :status, Types::String
14
+ attribute? :error, Types::Coercible::String
15
+
16
+ attribute :crypto_address do
17
+ attribute :currency, Types::String
18
+ end
19
+
20
+ attribute? :transactions, Types::Array do
21
+ attribute :transaction_type, Types::String
22
+ attribute :id, Types::Integer
23
+ end
24
+
25
+ attribute? :currency_sent do
26
+ attribute :amount, Types::String
27
+ end
28
+
29
+ attribute? :currency_received do
30
+ attribute :amount, Types::String
31
+ attribute? :amount_minus_fee, Types::String
32
+ end
33
+
34
+ def self.from_json(attributes)
35
+ attributes[:foreign_id] ||= attributes.dig(:crypto_address, :foreign_id)
36
+ new(attributes)
37
+ end
38
+
39
+ def blockchain_trx_id
40
+ transactions.find { |tr| tr.transaction_type == TNX_TYPE_BLOCKCHAIN }.id
41
+ end
42
+
43
+ def pending?
44
+ status == NOT_CONFIRMED
45
+ end
46
+
47
+ def cancelled?
48
+ status == CANCELLED
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'currency'
3
+
4
+ module CoinsPaid
5
+ module API
6
+ module CurrenciesList
7
+ module_function
8
+
9
+ class Response < Dry::Struct
10
+ attribute :data, Types::Array.of(Currency)
11
+ end
12
+
13
+ PATH = 'currencies/list'
14
+
15
+ def call
16
+ Response.new(data: Transport.post(PATH)).data
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CoinsPaid
4
+ module API
5
+ class Currency < Dry::Struct
6
+ transform_keys(&:to_sym)
7
+
8
+ attribute :id, Types::Integer
9
+ attribute :type, Types::String
10
+ attribute :currency, Types::String
11
+ attribute :minimum_amount, Types::JSON::Decimal
12
+ attribute :deposit_fee_percent, Types::JSON::Decimal
13
+ attribute :withdrawal_fee_percent, Types::JSON::Decimal
14
+ attribute :precision, Types::Integer
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CoinsPaid
4
+ module API
5
+ module Requester
6
+ extend self
7
+
8
+ def call(api, data)
9
+ request_data = api::Request.new(data)
10
+ Transport.post(api::PATH, request_data.to_hash)
11
+ .yield_self { |response| parse(response) }
12
+ .yield_self { |parsed_response| api::Response.new(parsed_response) }
13
+ end
14
+
15
+ private
16
+
17
+ def parse(response)
18
+ response.transform_keys { |key| key.to_s == 'id' ? :external_id : key.to_sym }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CoinsPaid
4
+ module API
5
+ module Signature
6
+ module_function
7
+
8
+ def check!(request_body:, key:, signature:)
9
+ key == API.public_key && signature == generate(request_body) ||
10
+ raise(InvalidSignatureError, 'Invalid signature')
11
+ end
12
+
13
+ def generate(request_body)
14
+ OpenSSL::HMAC.hexdigest('SHA512', API.secret_key, request_body)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CoinsPaid
4
+ module API
5
+ class TakeAddress
6
+ class Request < Dry::Struct
7
+ attribute :foreign_id, Types::Coercible::String
8
+ attribute :currency, Types::String
9
+ attribute :convert_to, Types::String
10
+ end
11
+
12
+ class Response < Dry::Struct
13
+ attribute :external_id, Types::Integer
14
+ attribute :address, Types::String
15
+ attribute :tag, Types::String.optional
16
+ end
17
+
18
+ PATH = 'addresses/take'
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CoinsPaid
4
+ module API
5
+ module Transport
6
+ extend self
7
+
8
+ def post(path, params = {})
9
+ post_request path, params.to_json
10
+ rescue Faraday::ParsingError => e
11
+ raise ConnectionError, e.response.body
12
+ rescue Faraday::Error => e
13
+ raise ConnectionError, e
14
+ end
15
+
16
+ private
17
+
18
+ def post_request(path, params)
19
+ response = http.post(path, params) do |req|
20
+ req.headers.merge!(
21
+ 'X-Processing-Key' => API.public_key,
22
+ 'X-Processing-Signature' => Signature.generate(params)
23
+ )
24
+ end
25
+
26
+ body = response.body
27
+ response.success? ? body['data'] : raise(ProcessingError, error_message(body))
28
+ end
29
+
30
+ def error_message(response_body)
31
+ response_body['error'] || response_body['errors'].values.first
32
+ end
33
+
34
+ def http
35
+ Faraday.new(url: URL) do |conn|
36
+ conn.request :json
37
+ conn.response :json
38
+ conn.adapter Faraday.default_adapter
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CoinsPaid
4
+ module API
5
+ module Types
6
+ include Dry.Types()
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CoinsPaid
4
+ module API
5
+ class Withdrawal
6
+ class Request < Dry::Struct
7
+ attribute :foreign_id, Types::Coercible::String
8
+ attribute :amount, Types::Coercible::String
9
+ attribute :currency, Types::String
10
+ attribute :convert_to, Types::String
11
+ attribute :address, Types::String
12
+ end
13
+
14
+ class Response < Dry::Struct
15
+ attribute :external_id, Types::Integer
16
+ attribute :receiver_amount, Types::Coercible::Float
17
+ end
18
+
19
+ PATH = 'withdrawal/crypto'
20
+ end
21
+ end
22
+ end
@@ -0,0 +1 @@
1
+ require_relative 'coins_paid/api'
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe CoinsPaid::API, '.callback' do
4
+ include_context 'coins paid callbacks'
5
+ let(:key) { double 'key' }
6
+ let(:signature) { double 'signature' }
7
+ let(:headers) { { 'X-Processing-Key' => key, 'X-Processing-Signature' => signature } }
8
+ subject(:callback) { described_class.callback(request_body, headers) }
9
+
10
+ before do
11
+ allow(CoinsPaid::API::Signature).to receive(:check!).with(request_body: request_body, key: key, signature: signature)
12
+ end
13
+
14
+ context 'when signature is invalid' do
15
+ let(:request_body) { { key: :value }.to_json }
16
+
17
+ before do
18
+ allow(CoinsPaid::API::CallbackData).to receive(:new)
19
+ allow(CoinsPaid::API::Signature).to receive(:check!).with(request_body: request_body, key: key, signature: signature).and_raise('error')
20
+ end
21
+
22
+ it 'raises error' do
23
+ expect { callback }.to raise_error('error')
24
+
25
+ expect(CoinsPaid::API::CallbackData).not_to have_received(:new)
26
+ end
27
+ end
28
+
29
+ context 'when request_body is deposit callback data' do
30
+ let(:request_body) { deposit_callback_body.to_json }
31
+ let(:expected_params) do
32
+ {
33
+ id: 2686510,
34
+ foreign_id: '1234',
35
+ type: 'deposit_exchange',
36
+ status: 'confirmed',
37
+ error: '',
38
+ crypto_address: {
39
+ currency: 'BTC'
40
+ },
41
+ transactions: [
42
+ { transaction_type: 'blockchain', id: 714576 },
43
+ { transaction_type: 'exchange', id: 714577 },
44
+ ],
45
+ currency_sent: { amount: '0.01000000' },
46
+ currency_received: {
47
+ amount_minus_fee: '90',
48
+ amount: '84.17070222'
49
+ }
50
+ }
51
+ end
52
+
53
+ it { is_expected.to be_struct_with_params(CoinsPaid::API::CallbackData, expected_params) }
54
+ end
55
+
56
+ context 'when request_body is cancelled deposit callback data' do
57
+ let(:request_body) { cancelled_deposit_callback_body.to_json }
58
+ let(:expected_params) do
59
+ {
60
+ id: 2686510,
61
+ foreign_id: '1234',
62
+ type: 'deposit_exchange',
63
+ status: 'cancelled',
64
+ error: 'Invalid params: expected a hex-encoded hash with 0x prefix.',
65
+ crypto_address: {
66
+ currency: 'BTC'
67
+ },
68
+ transactions: [
69
+ { transaction_type: 'blockchain', id: 714576 },
70
+ { transaction_type: 'exchange', id: 714577 },
71
+ ]
72
+ }
73
+ end
74
+
75
+ it { is_expected.to be_struct_with_params(CoinsPaid::API::CallbackData, expected_params) }
76
+ end
77
+
78
+ context 'when request_body is withdrawal callback data' do
79
+ let(:request_body) { withdrawal_callback_body.to_json }
80
+ let(:expected_params) do
81
+ {
82
+ id: 1,
83
+ type: 'withdrawal_exchange',
84
+ status: 'confirmed',
85
+ foreign_id: '20',
86
+ error: '',
87
+ crypto_address: {
88
+ currency: 'EUR'
89
+ },
90
+ transactions: [
91
+ { transaction_type: 'exchange', id: 1 },
92
+ { transaction_type: 'blockchain', id: 1 },
93
+ ],
94
+ currency_sent: { amount: '381' },
95
+ currency_received: {
96
+ amount: '0.01000000'
97
+ }
98
+ }
99
+ end
100
+
101
+ it { is_expected.to be_struct_with_params(CoinsPaid::API::CallbackData, expected_params) }
102
+ end
103
+
104
+ context 'when request_body is cancelled withdrawal callback data' do
105
+ let(:request_body) { cancelled_withdrawal_callback_body.to_json }
106
+ let(:expected_params) do
107
+ {
108
+ id: 2686510,
109
+ type: 'withdrawal_exchange',
110
+ status: 'cancelled',
111
+ foreign_id: '20',
112
+ error: 'Invalid params: expected a hex-encoded hash with 0x prefix.',
113
+ crypto_address: {
114
+ currency: 'EUR'
115
+ },
116
+ transactions: [
117
+ { transaction_type: 'exchange', id: 714576 },
118
+ { transaction_type: 'blockchain', id: 714577 },
119
+ ]
120
+ }
121
+ end
122
+
123
+ it { is_expected.to be_struct_with_params(CoinsPaid::API::CallbackData, expected_params) }
124
+ end
125
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe CoinsPaid::API::Signature, '#check!' do
4
+ let(:key) { 'publickey' }
5
+ let(:signature) { 'd2b3292793cb1f527dab4c9d8128356a0df7635aa1796a4d45276646ce914dcf29bb9244aed750a3a5b7d26aabb44ba560b05ed1233168107bed4ca684522508' }
6
+ let(:request_body) { { key: :value }.to_json }
7
+ subject(:check) { described_class.check!(key: key, signature: signature, request_body: request_body) }
8
+
9
+ context 'when key and signature are valid' do
10
+ it { is_expected.to be_truthy }
11
+ end
12
+
13
+ context 'when key is invalid' do
14
+ let(:key) { 'invalidkey' }
15
+
16
+ it 'raises invalid signature error' do
17
+ expect { check }.to raise_error(CoinsPaid::API::InvalidSignatureError)
18
+ end
19
+ end
20
+
21
+ context 'when signature is invalid' do
22
+ let(:signature) { 'invalidsignature' }
23
+
24
+ it 'raises invalid signature error' do
25
+ expect { check }.to raise_error(CoinsPaid::API::InvalidSignatureError)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './request_examples'
4
+
5
+ describe CoinsPaid::API, '.currencies_list' do
6
+ endpoint = 'https://app.coinspaid.com/api/v2/currencies/list'
7
+ include_context 'CoinsPaid API request'
8
+
9
+ let(:expected_currencies) do
10
+ [
11
+ {
12
+ currency: 'BTC',
13
+ deposit_fee_percent: 0.008,
14
+ id: 1,
15
+ minimum_amount: 0.0001,
16
+ precision: 8,
17
+ type: 'crypto',
18
+ withdrawal_fee_percent: 0.0
19
+ },
20
+ {
21
+ currency: 'LTC',
22
+ deposit_fee_percent: 0.008,
23
+ id: 2,
24
+ minimum_amount: 0.01,
25
+ precision: 8,
26
+ type: 'crypto',
27
+ withdrawal_fee_percent: 0.0
28
+ }
29
+ ]
30
+ end
31
+
32
+ subject(:response) { described_class.currencies_list }
33
+
34
+ let(:response_data) do
35
+ {
36
+ 'data' => [
37
+ {
38
+ 'currency' => 'BTC',
39
+ 'deposit_fee_percent' => '0.008',
40
+ 'id' => 1,
41
+ 'minimum_amount' => '0.00010000',
42
+ 'precision' => 8,
43
+ 'type' => 'crypto',
44
+ 'withdrawal_fee_percent' => '0'
45
+ },
46
+ {
47
+ 'currency' => 'LTC',
48
+ 'deposit_fee_percent' => '0.008',
49
+ 'id' => 2,
50
+ 'minimum_amount' => '0.01000000',
51
+ 'precision' => 8,
52
+ 'type' => 'crypto',
53
+ 'withdrawal_fee_percent' => '0'
54
+ }
55
+ ]
56
+ }
57
+ end
58
+
59
+ it 'returns valid response if successful' do
60
+ stub_request(:post, endpoint)
61
+ .with(body: '{}', headers: request_signature_headers)
62
+ .to_return(status: 200, body: response_data.to_json)
63
+
64
+ currencies = expected_currencies.map { |data| be_struct_with_params(described_class::Currency, data) }
65
+ expect(response).to match_array currencies
66
+ end
67
+
68
+ it_behaves_like 'CoinsPaid API error handling', endpoint: endpoint
69
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_context 'CoinsPaid API request' do |request_data: {}|
4
+ let(:signature) { 'c01dc0ffee' }
5
+ let(:request_signature_headers) do
6
+ {
7
+ 'X-Processing-Key' => described_class.public_key,
8
+ 'X-Processing-Signature' => signature
9
+ }
10
+ end
11
+
12
+ before do
13
+ allow(CoinsPaid::API::Signature).to receive(:generate).with(request_data.to_json).and_return signature
14
+ end
15
+ end
16
+
17
+ RSpec.shared_examples 'CoinsPaid API error handling' do |endpoint:, request_body: '{}'|
18
+ context 'when coins paid responded with validation errors' do
19
+ let(:response_data) do
20
+ {
21
+ 'errors' => {
22
+ 'field' => 'This field is wrong'
23
+ }
24
+ }
25
+ end
26
+
27
+ before do
28
+ stub_request(:post, endpoint)
29
+ .with(body: request_body)
30
+ .to_return(status: 400, body: response_data.to_json)
31
+ end
32
+
33
+ it 'raises processing error' do
34
+ expect { subject }.to raise_error(CoinsPaid::API::ProcessingError, 'This field is wrong')
35
+ end
36
+ end
37
+
38
+ context 'when coins paid responded with authorization error' do
39
+ let(:response_data) do
40
+ {
41
+ 'error' => 'Bad signature header',
42
+ 'code' => 'bad_header_signature'
43
+ }
44
+ end
45
+
46
+ before do
47
+ stub_request(:post, endpoint)
48
+ .with(body: request_body)
49
+ .to_return(status: 403, body: response_data.to_json)
50
+ end
51
+
52
+ it 'raises processing error' do
53
+ expect { subject }.to raise_error(CoinsPaid::API::ProcessingError, 'Bad signature header')
54
+ end
55
+ end
56
+
57
+ context 'when coins paid responds with internal server error' do
58
+ let(:response_data) { 'Internal server error' }
59
+
60
+ before do
61
+ stub_request(:post, endpoint)
62
+ .to_return(status: 500, body: response_data)
63
+ end
64
+
65
+ it 'raises processing error' do
66
+ expect { subject }.to raise_error(CoinsPaid::API::ConnectionError, 'Internal server error')
67
+ end
68
+ end
69
+
70
+ context 'when request timeout' do
71
+ before do
72
+ stub_request(:post, endpoint)
73
+ .to_timeout
74
+ end
75
+
76
+ it 'raises connection error' do
77
+ expect { subject }.to raise_error(CoinsPaid::API::ConnectionError, 'execution expired')
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,13 @@
1
+ ENV['COINS_PAID_PUBLIC_KEY'] = 'publickey'
2
+ ENV['COINS_PAID_SECRET_KEY'] = 'secretkey'
3
+
4
+ if ENV['CIRCLE_ARTIFACTS']
5
+ require 'simplecov'
6
+ SimpleCov.start
7
+ end
8
+
9
+ require 'webmock/rspec'
10
+ require 'pry'
11
+ require 'coins_paid_api'
12
+ require_relative 'support/shared_data'
13
+ require_relative 'support/struct_like'
@@ -0,0 +1,202 @@
1
+ shared_context 'coins paid callbacks' do
2
+ let(:deposit_callback_body) do
3
+ {
4
+ 'id' => 2686510,
5
+ 'type' => 'deposit_exchange',
6
+ 'crypto_address' => {
7
+ 'id' => 382270,
8
+ 'currency' => 'BTC',
9
+ 'convert_to' => 'EUR',
10
+ 'address' => '123abc',
11
+ 'tag' => nil,
12
+ 'foreign_id' => '1234'
13
+ },
14
+ 'currency_sent' => {
15
+ 'currency' => 'BTC',
16
+ 'amount' => '0.01000000'
17
+ },
18
+ 'currency_received' => {
19
+ 'currency' => 'EUR',
20
+ 'amount' => '84.17070222',
21
+ 'amount_minus_fee' => '90'
22
+ },
23
+ 'transactions' => [
24
+ {
25
+ 'id' => 714576,
26
+ 'currency' => 'BTC',
27
+ 'transaction_type' => 'blockchain',
28
+ 'type' => 'deposit',
29
+ 'address' => '31vnLqxVJ1iShJ5Ly586q8XKucECx12bZS',
30
+ 'tag' => nil,
31
+ 'amount' => '0.01000000',
32
+ 'txid' => '3a491da90a1ce5a318d0aeff6867ab98a03219abae29ed68d702291703c3538b',
33
+ 'riskscore' => '0.42',
34
+ 'confirmations' => '1'
35
+ },
36
+ {
37
+ 'id' => 714577,
38
+ 'currency' => 'BTC',
39
+ 'currency_to' => 'EUR',
40
+ 'transaction_type' => 'exchange',
41
+ 'type' => 'exchange',
42
+ 'amount' => '0.01000000',
43
+ 'amount_to' => '84.17070222'
44
+ }
45
+ ],
46
+ 'fees' => [
47
+ {
48
+ 'type' => 'exchange',
49
+ 'currency' => 'EUR',
50
+ 'amount' => '4.20853511'
51
+ }
52
+ ],
53
+ 'error' => nil,
54
+ 'status' => 'confirmed'
55
+ }
56
+ end
57
+
58
+ let(:cancelled_deposit_callback_body) do
59
+ {
60
+ 'id' => 2686510,
61
+ 'type' => 'deposit_exchange',
62
+ 'crypto_address' => {
63
+ 'id' => 382270,
64
+ 'currency' => 'BTC',
65
+ 'convert_to' => 'EUR',
66
+ 'address' => '123abc',
67
+ 'tag' => nil,
68
+ 'foreign_id' => '1234'
69
+ },
70
+ 'transactions' => [
71
+ {
72
+ 'id' => 714576,
73
+ 'currency' => 'BTC',
74
+ 'transaction_type' => 'blockchain',
75
+ 'type' => 'deposit',
76
+ 'address' => '31vnLqxVJ1iShJ5Ly586q8XKucECx12bZS',
77
+ 'tag' => nil,
78
+ 'amount' => '0.01000000',
79
+ 'txid' => '3a491da90a1ce5a318d0aeff6867ab98a03219abae29ed68d702291703c3538b',
80
+ 'riskscore' => '0.42',
81
+ 'confirmations' => '1'
82
+ },
83
+ {
84
+ 'id' => 714577,
85
+ 'currency' => 'BTC',
86
+ 'currency_to' => 'EUR',
87
+ 'transaction_type' => 'exchange',
88
+ 'type' => 'exchange',
89
+ 'amount' => '0.01000000',
90
+ 'amount_to' => '84.17070222'
91
+ }
92
+ ],
93
+ 'fees' => [
94
+ {
95
+ 'type' => 'exchange',
96
+ 'currency' => 'EUR',
97
+ 'amount' => '4.20853511'
98
+ }
99
+ ],
100
+ 'error' => 'Invalid params: expected a hex-encoded hash with 0x prefix.',
101
+ 'status' => 'cancelled'
102
+ }
103
+ end
104
+
105
+ let(:withdrawal_callback_body) do
106
+ {
107
+ 'id' => 1,
108
+ 'foreign_id' => '20',
109
+ 'type' => 'withdrawal_exchange',
110
+ 'crypto_address' => {
111
+ 'id' => 1,
112
+ 'currency' => 'EUR',
113
+ 'convert_to' => 'BTC',
114
+ 'address' => '1k2btnz8cqnfbphaq729mdj8w6g3w2nbbl',
115
+ 'tag' => nil
116
+ },
117
+ 'currency_sent' => {
118
+ 'currency' => 'EUR',
119
+ 'amount' => '381'
120
+ },
121
+ 'currency_received' => {
122
+ 'currency' => 'BTC',
123
+ 'amount' => '0.01000000'
124
+ },
125
+ 'transactions' => [
126
+ {
127
+ 'id' => 1,
128
+ 'currency' => 'EUR',
129
+ 'currency_to' => 'BTC',
130
+ 'transaction_type' => 'exchange',
131
+ 'type' => 'exchange',
132
+ 'amount' => 381,
133
+ 'amount_to' => 0.108823
134
+ },
135
+ {
136
+ 'id' => 1,
137
+ 'currency' => 'BTC',
138
+ 'transaction_type' => 'blockchain',
139
+ 'type' => 'withdrawal',
140
+ 'address' => '1k2btnz8cqnfbphaq729mdj8w6g3w2nbbl',
141
+ 'tag' => nil,
142
+ 'amount' => 0.108823,
143
+ 'txid' => 'aa3345b96389e126f1ce88a670d1b1e38f2c3f73fb3ecfff8d9da1b1ce6964a6',
144
+ 'confirmations' => 3
145
+ }
146
+ ],
147
+ 'fees' => [
148
+ {
149
+ 'type' => 'exchange',
150
+ 'currency' => 'EUR',
151
+ 'amount' => '3.04800000'
152
+ },
153
+ {
154
+ 'type' => 'mining',
155
+ 'currency' => 'BTC',
156
+ 'amount' => '0.00003990'
157
+ }
158
+ ],
159
+ 'error' => '',
160
+ 'status' => 'confirmed'
161
+ }
162
+ end
163
+
164
+ let(:cancelled_withdrawal_callback_body) do
165
+ {
166
+ 'id' => 2686510,
167
+ 'type' => 'withdrawal_exchange',
168
+ 'foreign_id' => '20',
169
+ 'crypto_address' => {
170
+ 'id' => 382270,
171
+ 'currency' => 'EUR',
172
+ 'address' => '1k2btnz8cqnfbphaq729mdj8w6g3w2nbbl',
173
+ 'tag' => nil
174
+ },
175
+ 'transactions' => [
176
+ {
177
+ 'id' => 714576,
178
+ 'currency' => 'BTC',
179
+ 'currency_to' => 'EUR',
180
+ 'transaction_type' => 'exchange',
181
+ 'type' => 'exchange',
182
+ 'amount' => '0.01000000',
183
+ 'amount_to' => '84.17070222'
184
+ },
185
+ {
186
+ 'id' => 714577,
187
+ 'currency' => 'BTC',
188
+ 'transaction_type' => 'blockchain',
189
+ 'type' => 'withdrawal',
190
+ 'address' => '31vnlqxvj1ishj5ly586q8xkucecx12bzs',
191
+ 'tag' => nil,
192
+ 'amount' => '0.01000000',
193
+ 'txid' => '3a491da90a1ce5a318d0aeff6867ab98a03219abae29ed68d702291703c3538b',
194
+ 'confirmations' => '0'
195
+ }
196
+ ],
197
+ 'fees' => [],
198
+ 'error' => 'Invalid params: expected a hex-encoded hash with 0x prefix.',
199
+ 'status' => 'cancelled'
200
+ }
201
+ end
202
+ end
@@ -0,0 +1,5 @@
1
+ RSpec::Matchers.define :be_struct_with_params do |class_name, params|
2
+ match do |actual|
3
+ actual.to_h == params && actual.class == class_name
4
+ end
5
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './request_examples'
4
+
5
+ describe CoinsPaid::API, '.take_address' do
6
+ endpoint = 'https://app.coinspaid.com/api/v2/addresses/take'
7
+ request_data = {
8
+ foreign_id: 'user-id:2048',
9
+ currency: 'BTC',
10
+ convert_to: 'EUR'
11
+ }
12
+ include_context 'CoinsPaid API request', request_data: request_data
13
+
14
+ let(:expected_address_attributes) do
15
+ {
16
+ external_id: 1,
17
+ address: '12983h13ro1hrt24it432t',
18
+ tag: 'tag-123'
19
+ }
20
+ end
21
+ subject(:take_address) { described_class.take_address(request_data) }
22
+
23
+ let(:response_data) do
24
+ {
25
+ 'data' => {
26
+ 'id' => 1,
27
+ 'currency' => 'BTC',
28
+ 'convert_to' => 'EUR',
29
+ 'address' => '12983h13ro1hrt24it432t',
30
+ 'tag' => 'tag-123',
31
+ 'foreign_id' => 'user-id:2048'
32
+ }
33
+ }
34
+ end
35
+
36
+ it 'returns valid response if successful' do
37
+ stub_request(:post, endpoint)
38
+ .with(body: request_data, headers: request_signature_headers)
39
+ .to_return(status: 201, body: response_data.to_json)
40
+
41
+ expect(take_address).to be_struct_with_params(CoinsPaid::API::TakeAddress::Response, expected_address_attributes)
42
+ end
43
+
44
+ it_behaves_like 'CoinsPaid API error handling', endpoint: endpoint, request_body: request_data
45
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './request_examples'
4
+
5
+ describe CoinsPaid::API, '.withdraw' do
6
+ endpoint = 'https://app.coinspaid.com/api/v2/withdrawal/crypto'
7
+ request_data = {
8
+ foreign_id: 'user-id:2048',
9
+ amount: '0.01',
10
+ currency: 'EUR',
11
+ convert_to: 'BTC',
12
+ address: 'abc123'
13
+ }
14
+ include_context 'CoinsPaid API request', request_data: request_data
15
+
16
+ let(:response_data) do
17
+ {
18
+ 'data' => {
19
+ 'id' => 1,
20
+ 'foreign_id' => 'user-id:2048',
21
+ 'type' => 'withdrawal',
22
+ 'status' => 'processing',
23
+ 'amount' => '10.00000000',
24
+ 'sender_amount' => '10.00000000',
25
+ 'sender_currency' => 'EUR',
26
+ 'receiver_amount' => '0.00100000',
27
+ 'receiver_currency' => 'BTC'
28
+ }
29
+ }
30
+ end
31
+ let(:expected_withdrawal_attributes) do
32
+ {
33
+ external_id: 1,
34
+ receiver_amount: 0.001
35
+ }
36
+ end
37
+ subject(:withdraw) { described_class.withdraw(request_data) }
38
+
39
+ context 'when response is successful' do
40
+ before do
41
+ stub_request(:post, endpoint)
42
+ .with(body: request_data, headers: request_signature_headers)
43
+ .to_return(status: 201, body: response_data.to_json)
44
+ end
45
+
46
+ it 'returns valid response' do
47
+ expect(withdraw).to be_struct_with_params(CoinsPaid::API::Withdrawal::Response, expected_withdrawal_attributes)
48
+ end
49
+ end
50
+ end
metadata ADDED
@@ -0,0 +1,153 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: coins_paid_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Artem Biserov(artembiserov)
8
+ - Oleg Ivanov(morhekil)
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2019-11-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: dry-initializer
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '3.0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '3.0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: dry-struct
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '1.0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '1.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: faraday
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '0.12'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '0.12'
56
+ - !ruby/object:Gem::Dependency
57
+ name: faraday_middleware
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '0.11'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '0.11'
70
+ - !ruby/object:Gem::Dependency
71
+ name: rspec
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '3.0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '3.0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: webmock
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '3.7'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '3.7'
98
+ description:
99
+ email:
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".circleci/config.yml"
105
+ - ".gitignore"
106
+ - ".rspec"
107
+ - Gemfile
108
+ - coins_paid_api.gemspec
109
+ - lib/coins_paid/api.rb
110
+ - lib/coins_paid/api/callback_data.rb
111
+ - lib/coins_paid/api/currencies_list.rb
112
+ - lib/coins_paid/api/currency.rb
113
+ - lib/coins_paid/api/requester.rb
114
+ - lib/coins_paid/api/signature.rb
115
+ - lib/coins_paid/api/take_address.rb
116
+ - lib/coins_paid/api/transport.rb
117
+ - lib/coins_paid/api/types.rb
118
+ - lib/coins_paid/api/withdrawal.rb
119
+ - lib/coins_paid_api.rb
120
+ - spec/callback_spec.rb
121
+ - spec/check_signature_spec.rb
122
+ - spec/currencies_list_spec.rb
123
+ - spec/request_examples.rb
124
+ - spec/spec_helper.rb
125
+ - spec/support/shared_data.rb
126
+ - spec/support/struct_like.rb
127
+ - spec/take_address_spec.rb
128
+ - spec/withdrawal_spec.rb
129
+ homepage:
130
+ licenses:
131
+ - Nonstandard
132
+ metadata: {}
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubyforge_project:
149
+ rubygems_version: 2.7.6
150
+ signing_key:
151
+ specification_version: 4
152
+ summary: Coins Paid Integration
153
+ test_files: []