coins_paid_api 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []