line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Finance::GDAX::API; |
2
|
|
|
|
|
|
|
our $VERSION = '0.01'; |
3
|
16
|
|
|
16
|
|
1162
|
use 5.20.0; |
|
16
|
|
|
|
|
54
|
|
4
|
16
|
|
|
16
|
|
94
|
use warnings; |
|
16
|
|
|
|
|
35
|
|
|
16
|
|
|
|
|
472
|
|
5
|
16
|
|
|
16
|
|
8041
|
use JSON; |
|
16
|
|
|
|
|
123059
|
|
|
16
|
|
|
|
|
125
|
|
6
|
16
|
|
|
16
|
|
2286
|
use Moose; |
|
16
|
|
|
|
|
33
|
|
|
16
|
|
|
|
|
140
|
|
7
|
16
|
|
|
16
|
|
135510
|
use REST::Client; |
|
16
|
|
|
|
|
573281
|
|
|
16
|
|
|
|
|
592
|
|
8
|
16
|
|
|
16
|
|
7625
|
use MIME::Base64; |
|
16
|
|
|
|
|
9086
|
|
|
16
|
|
|
|
|
1057
|
|
9
|
16
|
|
|
16
|
|
7280
|
use Digest::SHA qw(hmac_sha256_base64); |
|
16
|
|
|
|
|
44502
|
|
|
16
|
|
|
|
|
1302
|
|
10
|
16
|
|
|
16
|
|
5323
|
use Finance::GDAX::API::URL; |
|
16
|
|
|
|
|
125
|
|
|
16
|
|
|
|
|
977
|
|
11
|
16
|
|
|
16
|
|
5921
|
use namespace::autoclean; |
|
16
|
|
|
|
|
61331
|
|
|
16
|
|
|
|
|
121
|
|
12
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
has 'debug' => (is => 'rw', |
14
|
|
|
|
|
|
|
isa => 'Bool', |
15
|
|
|
|
|
|
|
default => 1, |
16
|
|
|
|
|
|
|
); |
17
|
|
|
|
|
|
|
has 'key' => (is => 'rw', |
18
|
|
|
|
|
|
|
isa => 'Str', |
19
|
|
|
|
|
|
|
default => sub {$ENV{GDAX_API_KEY} || ''}, |
20
|
|
|
|
|
|
|
lazy => 1, |
21
|
|
|
|
|
|
|
); |
22
|
|
|
|
|
|
|
has 'secret' => (is => 'rw', |
23
|
|
|
|
|
|
|
isa => 'Str', |
24
|
|
|
|
|
|
|
default => sub {$ENV{GDAX_API_SECRET} || ''}, |
25
|
|
|
|
|
|
|
lazy => 1, |
26
|
|
|
|
|
|
|
); |
27
|
|
|
|
|
|
|
has 'passphrase' => (is => 'rw', |
28
|
|
|
|
|
|
|
isa => 'Str', |
29
|
|
|
|
|
|
|
default => sub {$ENV{GDAX_API_PASSPHRASE} || ''}, |
30
|
|
|
|
|
|
|
lazy => 1, |
31
|
|
|
|
|
|
|
); |
32
|
|
|
|
|
|
|
has 'method' => (is => 'rw', |
33
|
|
|
|
|
|
|
isa => 'Str', |
34
|
|
|
|
|
|
|
default => 'POST', |
35
|
|
|
|
|
|
|
); |
36
|
|
|
|
|
|
|
has 'path' => (is => 'rw', |
37
|
|
|
|
|
|
|
isa => 'Str', |
38
|
|
|
|
|
|
|
); |
39
|
|
|
|
|
|
|
has 'body' => (is => 'rw', |
40
|
|
|
|
|
|
|
isa => 'Ref', |
41
|
|
|
|
|
|
|
); |
42
|
|
|
|
|
|
|
has 'timestamp' => (is => 'ro', |
43
|
|
|
|
|
|
|
isa => 'Int', |
44
|
|
|
|
|
|
|
default => sub { time }, |
45
|
|
|
|
|
|
|
); |
46
|
|
|
|
|
|
|
has 'timeout' => (is => 'rw', |
47
|
|
|
|
|
|
|
isa => 'Int', |
48
|
|
|
|
|
|
|
); |
49
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
has 'error' => (is => 'ro', |
51
|
|
|
|
|
|
|
isa => 'Str', |
52
|
|
|
|
|
|
|
writer => '_set_error', |
53
|
|
|
|
|
|
|
); |
54
|
|
|
|
|
|
|
has 'response_code' => (is => 'ro', |
55
|
|
|
|
|
|
|
isa => 'Int', |
56
|
|
|
|
|
|
|
writer => '_set_response_code', |
57
|
|
|
|
|
|
|
); |
58
|
|
|
|
|
|
|
has '_body_json' => (is => 'ro', |
59
|
|
|
|
|
|
|
isa => 'Maybe[Str]', |
60
|
|
|
|
|
|
|
writer => '_set_body_json', |
61
|
|
|
|
|
|
|
); |
62
|
|
|
|
|
|
|
|
63
|
|
|
|
|
|
|
sub send { |
64
|
2
|
|
|
2
|
1
|
6
|
my $self = shift; |
65
|
2
|
|
|
|
|
38
|
my $client = REST::Client->new; |
66
|
2
|
|
|
|
|
4321
|
my $url = Finance::GDAX::API::URL->new(debug => $self->debug); |
67
|
|
|
|
|
|
|
|
68
|
2
|
|
|
|
|
43
|
$url->add($self->path); |
69
|
|
|
|
|
|
|
|
70
|
2
|
|
|
|
|
42
|
$client->addHeader('CB-ACCESS-KEY', $self->key); |
71
|
2
|
|
|
|
|
37
|
$client->addHeader('CB-ACCESS-SIGN', $self->signature); |
72
|
2
|
|
|
|
|
56
|
$client->addHeader('CB-ACCESS-TIMESTAMP', $self->timestamp); |
73
|
2
|
|
|
|
|
56
|
$client->addHeader('CB-ACCESS-PASSPHRASE', $self->passphrase); |
74
|
2
|
|
|
|
|
22
|
$client->addHeader('Content-Type', 'application/json'); |
75
|
|
|
|
|
|
|
|
76
|
2
|
|
|
|
|
48
|
my $method = $self->method; |
77
|
2
|
50
|
|
|
|
38
|
$client->setTimetout($self->timeout) if $self->timeout; |
78
|
2
|
|
|
|
|
49
|
$self->_set_error(''); |
79
|
2
|
100
|
|
|
|
14
|
if ($method =~ /^(GET|DELETE)$/) { |
|
|
50
|
|
|
|
|
|
80
|
1
|
|
|
|
|
6
|
$client->$method($url->get); |
81
|
|
|
|
|
|
|
} |
82
|
|
|
|
|
|
|
elsif ($method eq 'POST') { |
83
|
1
|
|
|
|
|
7
|
$client->$method($url->get, $self->body_json); |
84
|
|
|
|
|
|
|
} |
85
|
|
|
|
|
|
|
|
86
|
2
|
|
|
|
|
1170669
|
my $content = JSON->new->decode($client->responseContent); |
87
|
2
|
|
|
|
|
146
|
$self->_set_response_code($client->responseCode); |
88
|
2
|
100
|
|
|
|
126
|
if ($self->response_code >= 400) { |
89
|
1
|
|
50
|
|
|
37
|
$self->_set_error( $$content{message} || 'no error message returned' ); |
90
|
|
|
|
|
|
|
} |
91
|
2
|
|
|
|
|
77
|
return $content; |
92
|
|
|
|
|
|
|
} |
93
|
|
|
|
|
|
|
|
94
|
|
|
|
|
|
|
sub signature { |
95
|
4
|
|
|
4
|
1
|
19
|
my $self = shift; |
96
|
4
|
|
|
|
|
35
|
my $json = JSON->new; |
97
|
4
|
|
|
|
|
94
|
my $data = $self->timestamp |
98
|
|
|
|
|
|
|
.$self->method |
99
|
|
|
|
|
|
|
.$self->path; |
100
|
4
|
100
|
|
|
|
93
|
$data .= $self->body_json if $self->body; |
101
|
4
|
|
|
|
|
69
|
my $digest = hmac_sha256_base64($data, decode_base64($self->secret)); |
102
|
4
|
|
|
|
|
18
|
while (length($digest) % 4) { |
103
|
4
|
|
|
|
|
11
|
$digest .= '='; |
104
|
|
|
|
|
|
|
} |
105
|
4
|
|
|
|
|
38
|
return $digest; |
106
|
|
|
|
|
|
|
} |
107
|
|
|
|
|
|
|
|
108
|
|
|
|
|
|
|
sub body_json { |
109
|
4
|
|
|
4
|
1
|
304
|
my $self = shift; |
110
|
4
|
100
|
|
|
|
73
|
return $self->_body_json if defined $self->_body_json; |
111
|
1
|
|
|
|
|
20
|
$self->_set_body_json(JSON->new->encode($self->body)); |
112
|
1
|
|
|
|
|
20
|
return $self->_body_json;; |
113
|
|
|
|
|
|
|
} |
114
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
sub external_secret { |
116
|
1
|
|
|
1
|
1
|
1311
|
my ($self, $filename, $fork) = @_; |
117
|
1
|
50
|
|
|
|
6
|
return unless $filename; |
118
|
1
|
|
|
|
|
17
|
my @valid_attributes = ('key', 'secret', 'passphrase'); |
119
|
1
|
|
|
|
|
4
|
my @input; |
120
|
1
|
50
|
|
|
|
5
|
if ($fork) { |
121
|
0
|
|
|
|
|
0
|
chomp(@input = `$filename`); |
122
|
|
|
|
|
|
|
} |
123
|
|
|
|
|
|
|
else { |
124
|
1
|
50
|
|
|
|
7
|
open FILE, "<", $filename or die "Cannot open $filename: $!"; |
125
|
1
|
|
|
|
|
82
|
chomp(@input = <FILE>); |
126
|
1
|
|
|
|
|
12
|
close FILE; |
127
|
|
|
|
|
|
|
} |
128
|
1
|
|
|
|
|
5
|
foreach (@input) { |
129
|
4
|
|
|
|
|
15
|
my ($key, $val) = split /:/; |
130
|
4
|
100
|
|
|
|
15
|
next if !$key; |
131
|
3
|
50
|
|
|
|
12
|
next if /^\s*\#/; |
132
|
3
|
50
|
|
|
|
58
|
unless (grep /^$key$/, @valid_attributes) { |
133
|
0
|
|
|
|
|
0
|
die "Bad attribute found in $filename ($key)"; |
134
|
|
|
|
|
|
|
} |
135
|
3
|
|
|
|
|
98
|
$self->$key($val); |
136
|
|
|
|
|
|
|
} |
137
|
1
|
|
|
|
|
5
|
return 1; |
138
|
|
|
|
|
|
|
} |
139
|
|
|
|
|
|
|
|
140
|
|
|
|
|
|
|
sub save_secrets_to_environment { |
141
|
0
|
|
|
0
|
1
|
|
my $self = shift; |
142
|
0
|
|
|
|
|
|
$ENV{GDAX_API_KEY} = $self->key; |
143
|
0
|
|
|
|
|
|
$ENV{GDAX_API_SECRET} = $self->secret; |
144
|
0
|
|
|
|
|
|
$ENV{GDAX_API_PASSPHRASE} = $self->passphrase; |
145
|
|
|
|
|
|
|
} |
146
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
__PACKAGE__->meta->make_immutable; |
148
|
|
|
|
|
|
|
1; |
149
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
=head1 NAME |
151
|
|
|
|
|
|
|
|
152
|
|
|
|
|
|
|
Finance::GDAX::API - Build and sign GDAX REST request |
153
|
|
|
|
|
|
|
|
154
|
|
|
|
|
|
|
=head1 SYNOPSIS |
155
|
|
|
|
|
|
|
|
156
|
|
|
|
|
|
|
$req = Finance::GDAX::API->new( |
157
|
|
|
|
|
|
|
key => 'My API Key', |
158
|
|
|
|
|
|
|
secret => 'My API Secret Key', |
159
|
|
|
|
|
|
|
passphrase => 'My API Passphrase'); |
160
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
$req->path('accounts'); |
162
|
|
|
|
|
|
|
$account_list = $req->send; |
163
|
|
|
|
|
|
|
|
164
|
|
|
|
|
|
|
# Use the more specific classes, for example Account: |
165
|
|
|
|
|
|
|
|
166
|
|
|
|
|
|
|
$account = Finance::GDAX::API::Account->new( |
167
|
|
|
|
|
|
|
key => 'My API Key', |
168
|
|
|
|
|
|
|
secret => 'My API Secret Key', |
169
|
|
|
|
|
|
|
passphrase => 'My API Passphrase'); |
170
|
|
|
|
|
|
|
$account_list = $account->get_all; |
171
|
|
|
|
|
|
|
$account_info = $account->get('89we-wefjbwe-wefwe-woowi'); |
172
|
|
|
|
|
|
|
|
173
|
|
|
|
|
|
|
# If you use Environment variables to store your secrects, you can |
174
|
|
|
|
|
|
|
# omit them in the constructors (see the Attributes below) |
175
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
$order = Finance::GDAX::API::Order->new; |
177
|
|
|
|
|
|
|
$orders = $order->list(['open','settled'], 'BTC-USD'); |
178
|
|
|
|
|
|
|
|
179
|
|
|
|
|
|
|
=head1 DESCRIPTION |
180
|
|
|
|
|
|
|
|
181
|
|
|
|
|
|
|
Creates a signed GDAX REST request - you need to provide the key, |
182
|
|
|
|
|
|
|
secret and passphrase attributes, or specify that they be provided by |
183
|
|
|
|
|
|
|
the external_secret method. |
184
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
All Finance::GDAX::API::* modules extend this class to implement their |
186
|
|
|
|
|
|
|
particular portion of the GDAX API. |
187
|
|
|
|
|
|
|
|
188
|
|
|
|
|
|
|
This is a low-level implementation of the GDAX API and complete, |
189
|
|
|
|
|
|
|
except for supporting result paging. |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
Return values are generally returned as references to arrays, hashes, |
192
|
|
|
|
|
|
|
arrays of hashes, hashes of arrays and all are documented within each |
193
|
|
|
|
|
|
|
method. |
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
All REST requests use https requests. |
196
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
=head1 ATTRIBUTES |
198
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
=head2 C<debug> (default: 1) |
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
Use debug mode (sandbox) or prouduction. By default requests are done |
202
|
|
|
|
|
|
|
with debug mode enabled which means connections will be made to the |
203
|
|
|
|
|
|
|
sandbox API. To do live data, you must set debug to 0. |
204
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
=head2 C<key> |
206
|
|
|
|
|
|
|
|
207
|
|
|
|
|
|
|
The GDAX API key. This defaults to the environment variable |
208
|
|
|
|
|
|
|
$ENV{GDAX_API_KEY} |
209
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
=head2 C<secret> |
211
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
The GDAX API secret key. This defaults to the environment variable |
213
|
|
|
|
|
|
|
$ENV{GDAX_API_SECRET} |
214
|
|
|
|
|
|
|
|
215
|
|
|
|
|
|
|
=head2 C<passphrase> |
216
|
|
|
|
|
|
|
|
217
|
|
|
|
|
|
|
The GDAX API passphrase. This defaults to the environment variable |
218
|
|
|
|
|
|
|
$ENV{GDAX_API_PASSPHRASE} |
219
|
|
|
|
|
|
|
|
220
|
|
|
|
|
|
|
=head2 C<error> |
221
|
|
|
|
|
|
|
|
222
|
|
|
|
|
|
|
Returns the text of an error message if there were any in the request. |
223
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
=head2 C<response_code> |
225
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
Returns the numeric HTTP status code of the request. |
227
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
=head2 C<method> (default: POST) |
229
|
|
|
|
|
|
|
|
230
|
|
|
|
|
|
|
REST method to use when data is submitted. Must be in upper-case. |
231
|
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
=head2 C<path> |
233
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
The URI path for the REST method, which must be set or errors will |
235
|
|
|
|
|
|
|
result. Leading '/' is not required. |
236
|
|
|
|
|
|
|
|
237
|
|
|
|
|
|
|
=head2 C<body> |
238
|
|
|
|
|
|
|
|
239
|
|
|
|
|
|
|
A reference to an array or hash that will be JSONified and represents |
240
|
|
|
|
|
|
|
the data being sent in the REST request body. This is optional. |
241
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
=head2 C<timestamp> (default: current unix epoch) |
243
|
|
|
|
|
|
|
|
244
|
|
|
|
|
|
|
An integer representing the Unix epoch of the request. This defaults |
245
|
|
|
|
|
|
|
to the current epoch time and will remain so as long as this object |
246
|
|
|
|
|
|
|
exists. |
247
|
|
|
|
|
|
|
|
248
|
|
|
|
|
|
|
=head2 C<timeout> (default: none) |
249
|
|
|
|
|
|
|
|
250
|
|
|
|
|
|
|
Integer time in seconds to wait for response to request. |
251
|
|
|
|
|
|
|
|
252
|
|
|
|
|
|
|
=head1 METHODS |
253
|
|
|
|
|
|
|
|
254
|
|
|
|
|
|
|
=head2 C<send> |
255
|
|
|
|
|
|
|
|
256
|
|
|
|
|
|
|
Sends the GDAX API request, returning the JSON response content as a |
257
|
|
|
|
|
|
|
perl data structure. Each Finance::GDAX::API::* class documents this |
258
|
|
|
|
|
|
|
structure (what to expect), as does the GDAX API (which will always be |
259
|
|
|
|
|
|
|
authoritative). |
260
|
|
|
|
|
|
|
|
261
|
|
|
|
|
|
|
=head2 C<external_secret> filename, fork? |
262
|
|
|
|
|
|
|
|
263
|
|
|
|
|
|
|
If you want to avoid hard-coding secrets into your code, this |
264
|
|
|
|
|
|
|
convenience method may be able to help. |
265
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
The method looks externally, either to a filename (default) or calls |
267
|
|
|
|
|
|
|
an executable file to provide the secrets via STDIN. |
268
|
|
|
|
|
|
|
|
269
|
|
|
|
|
|
|
Either way, the source of the secrets should provide key/value pairs |
270
|
|
|
|
|
|
|
delimited by colons, one per line: |
271
|
|
|
|
|
|
|
|
272
|
|
|
|
|
|
|
key:ThiSisMybiglongkey |
273
|
|
|
|
|
|
|
secret:HerEISmYSupeRSecret |
274
|
|
|
|
|
|
|
passphrase:andTHisiSMypassPhraSE |
275
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
There can be comments ("#" beginning a line), and blank lines. |
277
|
|
|
|
|
|
|
|
278
|
|
|
|
|
|
|
In other words, for exmple, if you cryptographically store your API |
279
|
|
|
|
|
|
|
credentials, you can create a small callable program that will decrypt |
280
|
|
|
|
|
|
|
them and provide them, so that they never live on disk unencrypted, |
281
|
|
|
|
|
|
|
and never show up in process listings: |
282
|
|
|
|
|
|
|
|
283
|
|
|
|
|
|
|
my $request = Finance::GDAX::API->new; |
284
|
|
|
|
|
|
|
$request->external_secret('/path/to/my_decryptor', 1); |
285
|
|
|
|
|
|
|
|
286
|
|
|
|
|
|
|
This would assign the key, secret and passphrase attributes for you by |
287
|
|
|
|
|
|
|
forking and running the 'my_decryptor' program. The 1 designates a |
288
|
|
|
|
|
|
|
fork, rather than a file read. |
289
|
|
|
|
|
|
|
|
290
|
|
|
|
|
|
|
This method will die easily if things aren't right. |
291
|
|
|
|
|
|
|
|
292
|
|
|
|
|
|
|
=head2 C<save_secrets_to_environment> |
293
|
|
|
|
|
|
|
|
294
|
|
|
|
|
|
|
Another convenience method that can be used to store your secrets into |
295
|
|
|
|
|
|
|
the volatile environment in which your perl is running, so that |
296
|
|
|
|
|
|
|
subsequent GDAX API object instances will not need to have the key, |
297
|
|
|
|
|
|
|
secret and passphrase set. |
298
|
|
|
|
|
|
|
|
299
|
|
|
|
|
|
|
You may not want to do this! It stores each attribute, "key", "secret" |
300
|
|
|
|
|
|
|
and "passphrase" to the environment variables "GDAX_API_KEY", |
301
|
|
|
|
|
|
|
"GDAX_API_SECRET" and "GDAX_API_PASSPHRASE", respectively. |
302
|
|
|
|
|
|
|
|
303
|
|
|
|
|
|
|
=head1 METHODS you probably don't need to worry about |
304
|
|
|
|
|
|
|
|
305
|
|
|
|
|
|
|
=head2 C<signature> |
306
|
|
|
|
|
|
|
|
307
|
|
|
|
|
|
|
Returns a string, base64-encoded representing the HMAC digest |
308
|
|
|
|
|
|
|
signature of the request, generated from the secrey key. |
309
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
=head2 C<body_json> |
311
|
|
|
|
|
|
|
|
312
|
|
|
|
|
|
|
Returns a string, the JSON-encoded representation of the data |
313
|
|
|
|
|
|
|
structure referenced by the "body" attribute. You don't normally need |
314
|
|
|
|
|
|
|
to look at this. |
315
|
|
|
|
|
|
|
|
316
|
|
|
|
|
|
|
=cut |
317
|
|
|
|
|
|
|
|
318
|
|
|
|
|
|
|
=head1 AUTHOR |
319
|
|
|
|
|
|
|
|
320
|
|
|
|
|
|
|
Mark Rushing <mark@orbislumen.net> |
321
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
323
|
|
|
|
|
|
|
|
324
|
|
|
|
|
|
|
This software is copyright (c) 2017 by Home Grown Systems, SPC. |
325
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
This is free software; you can redistribute it and/or modify it under |
327
|
|
|
|
|
|
|
the same terms as the Perl 5 programming language system itself. |
328
|
|
|
|
|
|
|
|
329
|
|
|
|
|
|
|
=cut |
330
|
|
|
|
|
|
|
|