chainalysis 0.1.0 → 0.2.0
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.
- 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