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 +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +133 -12
- data/lib/chainalysis/client.rb +177 -71
- data/lib/chainalysis/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1983f18acf9c5cbffa2043459bfab6ccecefa84e4dad29669e83428d41a65263
|
4
|
+
data.tar.gz: 3784792d2973049c1e76821605735c4016255ff7b6f9b77ee0cd3ab610dd4d5c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8ee8e31033ede5e47567edaed216379a3bcb89ef9b597dc7d145c1491fcbab211fecb18f79f725985c81285c53e9b1fb5e4d7419816954d5bf2bb1c0b806cc67
|
7
|
+
data.tar.gz: 2945234b5c597c1012aa47ff5fdfa31f7cc362d7f5c2c0271002571e9c970c9a3f4cbe081b8f45c43d5f55379f12fff51a450fb78c6dcffe0454dfcf863ebfef
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,39 +1,160 @@
|
|
1
|
-
# Chainalysis
|
1
|
+
# Chainalysis Ruby Client
|
2
2
|
|
3
|
-
|
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
|
-
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'chainalysis'
|
11
|
+
```
|
10
12
|
|
11
|
-
|
13
|
+
And then execute:
|
12
14
|
|
13
15
|
```bash
|
14
|
-
bundle
|
16
|
+
$ bundle install
|
15
17
|
```
|
16
18
|
|
17
|
-
|
19
|
+
Or install it yourself as:
|
18
20
|
|
19
21
|
```bash
|
20
|
-
gem install
|
22
|
+
$ gem install chainalysis
|
21
23
|
```
|
22
24
|
|
23
25
|
## Usage
|
24
26
|
|
25
|
-
|
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`.
|
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
|
-
|
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).
|
data/lib/chainalysis/client.rb
CHANGED
@@ -11,10 +11,11 @@ module Chainalysis
|
|
11
11
|
class RateLimitError < Error; end
|
12
12
|
class ApiError < Error; end
|
13
13
|
|
14
|
-
class
|
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.
|
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
|
-
|
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
|
-
|
42
|
-
|
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
|
-
|
109
|
-
|
110
|
-
def
|
111
|
-
@
|
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
|
167
|
-
|
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
|
data/lib/chainalysis/version.rb
CHANGED