File Coverage

lib/Finance/GDAX/API.pm
Criterion Covered Total %
statement 73 79 92.4
branch 17 24 70.8
condition 1 2 50.0
subroutine 13 14 92.8
pod 5 5 100.0
total 109 124 87.9


line stmt bran cond sub pod time code
1             package Finance::GDAX::API;
2             our $VERSION = '0.01';
3 16     16   1947 use 5.20.0;
  16         94  
4 16     16   100 use warnings;
  16         33  
  16         537  
5 16     16   8138 use JSON;
  16         129165  
  16         113  
6 16     16   2224 use Moose;
  16         36  
  16         137  
7 16     16   124826 use REST::Client;
  16         591655  
  16         606  
8 16     16   8249 use MIME::Base64;
  16         9389  
  16         1079  
9 16     16   7479 use Digest::SHA qw(hmac_sha256_base64);
  16         44306  
  16         1290  
10 16     16   5096 use Finance::GDAX::API::URL;
  16         66  
  16         889  
11 16     16   5721 use namespace::autoclean;
  16         63997  
  16         118  
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 7 my $self = shift;
65 2         52 my $client = REST::Client->new;
66 2         5828 my $url = Finance::GDAX::API::URL->new(debug => $self->debug);
67            
68 2         63 $url->add($self->path);
69            
70 2         63 $client->addHeader('CB-ACCESS-KEY', $self->key);
71 2         54 $client->addHeader('CB-ACCESS-SIGN', $self->signature);
72 2         78 $client->addHeader('CB-ACCESS-TIMESTAMP', $self->timestamp);
73 2         79 $client->addHeader('CB-ACCESS-PASSPHRASE', $self->passphrase);
74 2         32 $client->addHeader('Content-Type', 'application/json');
75              
76 2         86 my $method = $self->method;
77 2 50       137 $client->setTimetout($self->timeout) if $self->timeout;
78 2         99 $self->_set_error('');
79 2 100       18 if ($method =~ /^(GET|DELETE)$/) {
    50          
80 1         6 $client->$method($url->get);
81             }
82             elsif ($method eq 'POST') {
83 1         8 $client->$method($url->get, $self->body_json);
84             }
85              
86 2         1193709 my $content = JSON->new->decode($client->responseContent);
87 2         114 $self->_set_response_code($client->responseCode);
88 2 100       80 if ($self->response_code >= 400) {
89 1   50     44 $self->_set_error( $$content{message} || 'no error message returned' );
90             }
91 2         78 return $content;
92             }
93              
94             sub signature {
95 4     4 1 25 my $self = shift;
96 4         51 my $json = JSON->new;
97 4         151 my $data = $self->timestamp
98             .$self->method
99             .$self->path;
100 4 100       117 $data .= $self->body_json if $self->body;
101 4         117 my $digest = hmac_sha256_base64($data, decode_base64($self->secret));
102 4         26 while (length($digest) % 4) {
103 4         14 $digest .= '=';
104             }
105 4         53 return $digest;
106             }
107              
108             sub body_json {
109 4     4 1 748 my $self = shift;
110 4 100       129 return $self->_body_json if defined $self->_body_json;
111 1         38 $self->_set_body_json(JSON->new->encode($self->body));
112 1         41 return $self->_body_json;;
113             }
114              
115             sub external_secret {
116 1     1 1 12818 my ($self, $filename, $fork) = @_;
117 1 50       10 return unless $filename;
118 1         28 my @valid_attributes = ('key', 'secret', 'passphrase');
119 1         5 my @input;
120 1 50       7 if ($fork) {
121 0         0 chomp(@input = `$filename`);
122             }
123             else {
124 1 50       11 open FILE, "<", $filename or die "Cannot open $filename: $!";
125 1         144 chomp(@input = <FILE>);
126 1         68 close FILE;
127             }
128 1         7 foreach (@input) {
129 4         23 my ($key, $val) = split /:/;
130 4 100       18 next if !$key;
131 3 50       16 next if /^\s*\#/;
132 3 50       97 unless (grep /^$key$/, @valid_attributes) {
133 0         0 die "Bad attribute found in $filename ($key)";
134             }
135 3         147 $self->$key($val);
136             }
137 1         6 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