chainalysis 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3559d0827651183fd3adc8bff46df276d1821ee8bf6c16af0be8d7418ad55f5
4
- data.tar.gz: ea500373a881c4ff215d04f687d0eb0c18aab2e68237facf4576585ce75facb3
3
+ metadata.gz: 1983f18acf9c5cbffa2043459bfab6ccecefa84e4dad29669e83428d41a65263
4
+ data.tar.gz: 3784792d2973049c1e76821605735c4016255ff7b6f9b77ee0cd3ab610dd4d5c
5
5
  SHA512:
6
- metadata.gz: da082b2fe769d535bb2967c6e49ad40f70a460fb39378eadf3ec1389707c04e7e67942daee81a12f1d506269dc0936b52aeb356afb86feffdfa00adb6970c7a3
7
- data.tar.gz: 164c68e2cdea0a703c9887f171c18bcc40c827dbe1eca796faf04820de0e356c919cb9979f4863171cb1c629bd5b69ec547a80dd8b1c07248779ade6fa087b9b
6
+ metadata.gz: 8ee8e31033ede5e47567edaed216379a3bcb89ef9b597dc7d145c1491fcbab211fecb18f79f725985c81285c53e9b1fb5e4d7419816954d5bf2bb1c0b806cc67
7
+ data.tar.gz: 2945234b5c597c1012aa47ff5fdfa31f7cc362d7f5c2c0271002571e9c970c9a3f4cbe081b8f45c43d5f55379f12fff51a450fb78c6dcffe0454dfcf863ebfef
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2025-01-11
4
+
5
+ - Added support for v1 endpoints
6
+
3
7
  ## [0.1.0] - 2025-01-11
4
8
 
5
9
  - Initial release
data/README.md CHANGED
@@ -1,39 +1,160 @@
1
- # Chainalysis
1
+ # Chainalysis Ruby Client
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
4
-
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/chainalysis`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ A Ruby wrapper for the Chainalysis Transaction Monitoring API. This client library provides a simple, intuitive interface to interact with Chainalysis's API services.
6
4
 
7
5
  ## Installation
8
6
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'chainalysis'
11
+ ```
10
12
 
11
- Install the gem and add to the application's Gemfile by executing:
13
+ And then execute:
12
14
 
13
15
  ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
16
+ $ bundle install
15
17
  ```
16
18
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
19
+ Or install it yourself as:
18
20
 
19
21
  ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
22
+ $ gem install chainalysis
21
23
  ```
22
24
 
23
25
  ## Usage
24
26
 
25
- TODO: Write usage instructions here
27
+ First, initialize a client with your API key:
28
+
29
+ ```ruby
30
+ require 'chainalysis'
31
+
32
+ client = Chainalysis::Client.new(api_key: 'your_api_key')
33
+ ```
34
+
35
+ ### Registering a Transfer
36
+
37
+ ```ruby
38
+ # Register a new transfer
39
+ response = client.register_transfer(
40
+ user_id: 'user123',
41
+ network: 'Bitcoin',
42
+ asset: 'BTC',
43
+ transfer_reference: 'tx_hash:address',
44
+ direction: 'received',
45
+ # Optional parameters
46
+ transfer_timestamp: '2024-01-10T15:30:00Z',
47
+ asset_amount: 1.5,
48
+ asset_price: 45000.00,
49
+ asset_denomination: 'USD'
50
+ )
51
+
52
+ # The response includes an externalId that you can use to query the transfer
53
+ external_id = response['externalId']
54
+ ```
55
+
56
+ ### Managing Transfers
57
+
58
+ ```ruby
59
+ # Get transfer details
60
+ transfer = client.get_transfer(external_id: 'transfer_external_id')
61
+
62
+ # Get transfer exposures
63
+ exposures = client.get_transfer_exposures(external_id: 'transfer_external_id')
64
+
65
+ # Get transfer alerts
66
+ alerts = client.get_transfer_alerts(external_id: 'transfer_external_id')
67
+
68
+ # Get transfer network identifications
69
+ identifications = client.get_transfer_network_identifications(external_id: 'transfer_external_id')
70
+ ```
71
+
72
+ ### Managing Withdrawal Attempts
73
+
74
+ ```ruby
75
+ # Register a withdrawal attempt
76
+ response = client.register_withdrawal_attempt(
77
+ user_id: 'user123',
78
+ network: 'Bitcoin',
79
+ asset: 'BTC',
80
+ address: '1EM4e8eu2S2RQrbS8C6aYnunWpkAwQ8GtG',
81
+ attempt_identifier: 'withdrawal_001',
82
+ asset_amount: 2.5,
83
+ attempt_timestamp: '2024-01-10T15:30:00Z'
84
+ )
85
+
86
+ # Get withdrawal attempt details
87
+ withdrawal = client.get_withdrawal_attempt(external_id: 'withdrawal_external_id')
88
+
89
+ # Get withdrawal attempt exposures
90
+ exposures = client.get_withdrawal_attempt_exposures(external_id: 'withdrawal_external_id')
91
+
92
+ # Get withdrawal attempt alerts
93
+ alerts = client.get_withdrawal_attempt_alerts(external_id: 'withdrawal_external_id')
94
+
95
+ # Get high risk addresses
96
+ addresses = client.get_withdrawal_attempt_high_risk_addresses(external_id: 'withdrawal_external_id')
97
+
98
+ # Get network identifications
99
+ identifications = client.get_withdrawal_attempt_network_identifications(external_id: 'withdrawal_external_id')
100
+ ```
101
+
102
+ ### Categories and Administration
103
+
104
+ ```ruby
105
+ # Get all categories
106
+ categories = client.get_categories
107
+
108
+ # Get internal users (requires ORGADMIN permission)
109
+ users = client.get_internal_users
110
+ ```
111
+
112
+ ## Error Handling
113
+
114
+ The client includes custom error classes for different types of API errors:
115
+
116
+ ```ruby
117
+ begin
118
+ client.get_transfer(external_id: 'invalid_id')
119
+ rescue Chainalysis::NotFoundError => e
120
+ puts "Transfer not found: #{e.message}"
121
+ rescue Chainalysis::AuthenticationError => e
122
+ puts "Authentication failed: #{e.message}"
123
+ rescue Chainalysis::RateLimitError => e
124
+ puts "Rate limit exceeded: #{e.message}"
125
+ rescue Chainalysis::BadRequestError => e
126
+ puts "Bad request: #{e.message}"
127
+ rescue Chainalysis::ApiError => e
128
+ puts "API error: #{e.message}"
129
+ end
130
+ ```
26
131
 
27
132
  ## Development
28
133
 
29
134
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
135
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
136
+ To install this gem onto your local machine, run `bundle exec rake install`.
137
+
138
+ ### Running Tests
139
+
140
+ ```bash
141
+ $ bundle exec rspec
142
+ ```
143
+
144
+ Tests use VCR to record and replay HTTP interactions. To record new interactions, delete the corresponding cassette file and run the tests.
32
145
 
33
146
  ## Contributing
34
147
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/chainalysis.
148
+ 1. Fork it
149
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
150
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
151
+ 4. Push to the branch (`git push origin my-new-feature`)
152
+ 5. Create a new Pull Request
36
153
 
37
154
  ## License
38
155
 
39
156
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
157
+
158
+ ## Code of Conduct
159
+
160
+ Everyone interacting in the Chainalysis Ruby Client project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
@@ -11,10 +11,11 @@ module Chainalysis
11
11
  class RateLimitError < Error; end
12
12
  class ApiError < Error; end
13
13
 
14
- class Client
14
+ # Base client class handling common functionality
15
+ class BaseClient
15
16
  BASE_URL = 'https://api.chainalysis.com/api/kyt'
16
17
  ADMIN_URL = 'https://api.chainalysis.com/admin'
17
- VERSION = '0.1.0'
18
+ VERSION = '0.2.0'
18
19
 
19
20
  attr_reader :api_key, :adapter
20
21
 
@@ -24,7 +25,171 @@ module Chainalysis
24
25
  @stubs = stubs
25
26
  end
26
27
 
27
- # Transfer Registration
28
+ protected
29
+
30
+ def client
31
+ @client ||= Faraday.new(url: BASE_URL) do |conn|
32
+ conn.headers['Token'] = api_key
33
+ conn.headers['Accept'] = 'application/json'
34
+ conn.headers['Content-Type'] = 'application/json'
35
+ conn.adapter adapter, @stubs
36
+ end
37
+ end
38
+
39
+ def admin_client
40
+ @admin_client ||= Faraday.new(url: ADMIN_URL) do |conn|
41
+ conn.headers['Token'] = api_key
42
+ conn.headers['Accept'] = 'application/json'
43
+ conn.headers['Content-Type'] = 'application/json'
44
+ conn.adapter adapter, @stubs
45
+ end
46
+ end
47
+
48
+ def handle_response(response)
49
+ case response.status
50
+ when 200, 201, 202
51
+ return {} if response.body.empty?
52
+
53
+ JSON.parse(response.body)
54
+ when 400
55
+ raise BadRequestError, error_message(response)
56
+ when 403
57
+ raise AuthenticationError, error_message(response)
58
+ when 404
59
+ raise NotFoundError, error_message(response)
60
+ when 429
61
+ raise RateLimitError, error_message(response)
62
+ else
63
+ raise ApiError, error_message(response)
64
+ end
65
+ end
66
+
67
+ def error_message(response)
68
+ return response.body if response.body.empty?
69
+
70
+ error = JSON.parse(response.body)
71
+ error['message'] || error['error'] || response.body
72
+ rescue JSON::ParserError
73
+ response.body
74
+ end
75
+
76
+ def get_request(url, params = {}, admin: false)
77
+ client = admin ? admin_client : self.client
78
+ response = client.get(url) do |req|
79
+ req.params = params if params
80
+ end
81
+ handle_response(response)
82
+ end
83
+
84
+ def post_request(url, body = {})
85
+ response = client.post(url) do |req|
86
+ req.body = JSON.generate(body) unless body.empty?
87
+ end
88
+ handle_response(response)
89
+ end
90
+
91
+ def delete_request(url)
92
+ response = client.delete(url)
93
+ handle_response(response)
94
+ end
95
+ end
96
+
97
+ # Client for V1 API endpoints
98
+ class V1Client < BaseClient
99
+ # Transfer endpoints
100
+ def register_received_transfer(user_id:, transfers:)
101
+ post_request("v1/users/#{user_id}/transfers/received", transfers)
102
+ end
103
+
104
+ def get_received_transfers(user_id:, limit: nil, offset: nil)
105
+ params = {}
106
+ params[:limit] = limit if limit
107
+ params[:offset] = offset if offset
108
+ get_request("v1/users/#{user_id}/transfers/received", params)
109
+ end
110
+
111
+ def register_sent_transfer(user_id:, transfers:)
112
+ post_request("v1/users/#{user_id}/transfers/sent", transfers)
113
+ end
114
+
115
+ def get_sent_transfers(user_id:, limit: nil, offset: nil)
116
+ params = {}
117
+ params[:limit] = limit if limit
118
+ params[:offset] = offset if offset
119
+ get_request("v1/users/#{user_id}/transfers/sent", params)
120
+ end
121
+
122
+ # Withdrawal address endpoints
123
+ def register_withdrawal_addresses(user_id:, addresses:)
124
+ post_request("v1/users/#{user_id}/withdrawaladdresses", addresses)
125
+ end
126
+
127
+ def get_withdrawal_addresses(user_id:, limit: nil, offset: nil)
128
+ params = {}
129
+ params[:limit] = limit if limit
130
+ params[:offset] = offset if offset
131
+ get_request("v1/users/#{user_id}/withdrawaladdresses", params)
132
+ end
133
+
134
+ def delete_withdrawal_address(user_id:, asset:, address:)
135
+ delete_request("v1/users/#{user_id}/withdrawaladdresses/#{asset}/#{address}")
136
+ end
137
+
138
+ # Deposit address endpoints
139
+ def register_deposit_addresses(user_id:, addresses:)
140
+ post_request("v1/users/#{user_id}/depositaddresses", addresses)
141
+ end
142
+
143
+ def get_deposit_addresses(user_id:, limit: nil, offset: nil)
144
+ params = {}
145
+ params[:limit] = limit if limit
146
+ params[:offset] = offset if offset
147
+ get_request("v1/users/#{user_id}/depositaddresses", params)
148
+ end
149
+
150
+ def delete_deposit_address(user_id:, asset:, address:)
151
+ delete_request("v1/users/#{user_id}/depositaddresses/#{asset}/#{address}")
152
+ end
153
+
154
+ # Alert endpoints
155
+ def get_alerts(params = {})
156
+ get_request('v1/alerts/', params)
157
+ end
158
+
159
+ def assign_alert(alert_identifier:, alert_assignee:)
160
+ post_request("v1/alerts/#{alert_identifier}/assignment",
161
+ { alertAssignee: alert_assignee })
162
+ end
163
+
164
+ def update_alert_status(alert_identifier:, status:, comment: nil)
165
+ body = { status: status }
166
+ body[:comment] = comment if comment
167
+ post_request("v1/alerts/#{alert_identifier}/statuses", body)
168
+ end
169
+
170
+ def get_alert_activity(alert_identifier:)
171
+ get_request("v1/alerts/#{alert_identifier}/activity")
172
+ end
173
+
174
+ # User endpoints
175
+ def get_users(limit: nil, offset: nil)
176
+ params = {}
177
+ params[:limit] = limit if limit
178
+ params[:offset] = offset if offset
179
+ get_request('v1/users/', params)
180
+ end
181
+
182
+ def get_user(user_id:)
183
+ get_request("v1/users/#{user_id}")
184
+ end
185
+
186
+ def rename_users(renames:)
187
+ post_request('v1/users/rename', renames)
188
+ end
189
+ end
190
+
191
+ # Client for V2 API endpoints
192
+ class V2Client < BaseClient
28
193
  def register_transfer(user_id:, network:, asset:, transfer_reference:, direction:, **options)
29
194
  post_request(
30
195
  "v2/users/#{user_id}/transfers",
@@ -38,9 +203,8 @@ module Chainalysis
38
203
  )
39
204
  end
40
205
 
41
- # Withdrawal Attempt Registration
42
- def register_withdrawal_attempt(user_id:, network:, asset:, address:, attempt_identifier:, asset_amount:,
43
- attempt_timestamp:, **options)
206
+ def register_withdrawal_attempt(user_id:, network:, asset:, address:, attempt_identifier:,
207
+ asset_amount:, attempt_timestamp:, **options)
44
208
  post_request(
45
209
  "v2/users/#{user_id}/withdrawal-attempts",
46
210
  {
@@ -55,7 +219,6 @@ module Chainalysis
55
219
  )
56
220
  end
57
221
 
58
- # Transfer Endpoints
59
222
  def get_transfer(external_id:, format_type: nil)
60
223
  params = { format_type: format_type } if format_type
61
224
  get_request("v2/transfers/#{external_id}", params)
@@ -73,7 +236,6 @@ module Chainalysis
73
236
  get_request("v2/transfers/#{external_id}/network-identifications")
74
237
  end
75
238
 
76
- # Withdrawal Attempt Endpoints
77
239
  def get_withdrawal_attempt(external_id:, format_type: nil)
78
240
  params = { format_type: format_type } if format_type
79
241
  get_request("v2/withdrawal-attempts/#{external_id}", params)
@@ -95,79 +257,23 @@ module Chainalysis
95
257
  get_request("v2/withdrawal-attempts/#{external_id}/network-identifications")
96
258
  end
97
259
 
98
- # Categories
99
260
  def get_categories
100
261
  get_request('v2/categories')
101
262
  end
102
263
 
103
- # Administration
104
264
  def get_internal_users
105
265
  get_request('organization/users', admin: true)
106
266
  end
267
+ end
107
268
 
108
- private
109
-
110
- def client
111
- @client ||= Faraday.new(url: BASE_URL) do |conn|
112
- conn.headers['Token'] = api_key
113
- conn.headers['Accept'] = 'application/json'
114
- conn.headers['Content-Type'] = 'application/json'
115
-
116
- conn.adapter adapter, @stubs
117
- end
118
- end
119
-
120
- def admin_client
121
- @admin_client ||= Faraday.new(url: ADMIN_URL) do |conn|
122
- conn.headers['Token'] = api_key
123
- conn.headers['Accept'] = 'application/json'
124
- conn.headers['Content-Type'] = 'application/json'
125
-
126
- conn.adapter adapter, @stubs
127
- end
128
- end
129
-
130
- def handle_response(response)
131
- case response.status
132
- when 200, 201, 202
133
- return {} if response.body.empty?
134
-
135
- JSON.parse(response.body)
136
- when 400
137
- raise BadRequestError, error_message(response)
138
- when 403
139
- raise AuthenticationError, error_message(response)
140
- when 404
141
- raise NotFoundError, error_message(response)
142
- when 429
143
- raise RateLimitError, error_message(response)
144
- else
145
- raise ApiError, error_message(response)
146
- end
147
- end
148
-
149
- def error_message(response)
150
- return response.body if response.body.empty?
151
-
152
- error = JSON.parse(response.body)
153
- error['message'] || error['error'] || response.body
154
- rescue JSON::ParserError
155
- response.body
156
- end
157
-
158
- def get_request(url, params = {}, admin: false)
159
- client = admin ? admin_client : self.client
160
- response = client.get(url) do |req|
161
- req.params = params if params
162
- end
163
- handle_response(response)
269
+ # Main client class that provides access to both V1 and V2 clients
270
+ class Client < BaseClient
271
+ def v1
272
+ @v1 ||= V1Client.new(api_key: api_key, adapter: adapter, stubs: @stubs)
164
273
  end
165
274
 
166
- def post_request(url, body = {})
167
- response = client.post(url) do |req|
168
- req.body = JSON.generate(body) unless body.empty?
169
- end
170
- handle_response(response)
275
+ def v2
276
+ @v2 ||= V2Client.new(api_key: api_key, adapter: adapter, stubs: @stubs)
171
277
  end
172
278
  end
173
279
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Chainalysis
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chainalysis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Raghav Sood