File Coverage

blib/lib/Finance/LocalBitcoins/API.pm
Criterion Covered Total %
statement 212 247 85.8
branch 15 30 50.0
condition 6 9 66.6
subroutine 74 99 74.7
pod 5 54 9.2
total 312 439 71.0


line stmt bran cond sub pod time code
1             package Finance::LocalBitcoins::API;
2              
3 1     1   22236 use 5.014002;
  1         4  
  1         36  
4 1     1   6 use strict;
  1         2  
  1         31  
5 1     1   6 use warnings;
  1         8  
  1         54  
6              
7             our $VERSION = '0.01';
8              
9 1     1   7 use constant DEBUG => 0;
  1         2  
  1         72  
10 1     1   5 use constant VERBOSE => 0;
  1         2  
  1         44  
11              
12             # you can use a lower version, but then you are responsible for SSL cert verification code...
13 1     1   1039 use LWP::UserAgent 6;
  1         46608  
  1         29  
14 1     1   11 use URI;
  1         2  
  1         18  
15 1     1   930 use JSON;
  1         12072  
  1         6  
16 1     1   1060 use Data::Dumper;
  1         7584  
  1         90  
17              
18             ## PUBLIC requests..
19 1     1   662 use Finance::LocalBitcoins::API::Request::Ticker;
  1         5  
  1         30  
20 1     1   684 use Finance::LocalBitcoins::API::Request::TradeBook;
  1         4  
  1         29  
21 1     1   657 use Finance::LocalBitcoins::API::Request::OrderBook;
  1         3  
  1         27  
22              
23             # PRIVATE requests..
24 1     1   693 use Finance::LocalBitcoins::API::Request::User;
  1         4  
  1         29  
25 1     1   725 use Finance::LocalBitcoins::API::Request::Me;
  1         3  
  1         26  
26 1     1   611 use Finance::LocalBitcoins::API::Request::Pin;
  1         3  
  1         26  
27 1     1   640 use Finance::LocalBitcoins::API::Request::Dash;
  1         3  
  1         26  
28 1     1   565 use Finance::LocalBitcoins::API::Request::Wallet;
  1         3  
  1         52  
29 1     1   595 use Finance::LocalBitcoins::API::Request::Balance;
  1         4  
  1         25  
30 1     1   591 use Finance::LocalBitcoins::API::Request::ReleaseEscrow;
  1         3  
  1         31  
31 1     1   673 use Finance::LocalBitcoins::API::Request::Paid;
  1         4  
  1         29  
32 1     1   770 use Finance::LocalBitcoins::API::Request::Messages;
  1         3  
  1         27  
33 1     1   593 use Finance::LocalBitcoins::API::Request::Message;
  1         2  
  1         26  
34 1     1   1082 use Finance::LocalBitcoins::API::Request::Dispute;
  1         4  
  1         36  
35 1     1   867 use Finance::LocalBitcoins::API::Request::Cancel;
  1         4  
  1         29  
36 1     1   615 use Finance::LocalBitcoins::API::Request::Fund;
  1         3  
  1         28  
37 1     1   757 use Finance::LocalBitcoins::API::Request::NewContact;
  1         2  
  1         27  
38 1     1   615 use Finance::LocalBitcoins::API::Request::Contact;
  1         6  
  1         28  
39 1     1   641 use Finance::LocalBitcoins::API::Request::Contacts;
  1         3  
  1         26  
40 1     1   617 use Finance::LocalBitcoins::API::Request::Send;
  1         4  
  1         27  
41 1     1   632 use Finance::LocalBitcoins::API::Request::SendPin;
  1         3  
  1         27  
42 1     1   641 use Finance::LocalBitcoins::API::Request::Address;
  1         13  
  1         28  
43 1     1   593 use Finance::LocalBitcoins::API::Request::Logout;
  1         2  
  1         27  
44 1     1   707 use Finance::LocalBitcoins::API::Request::Ads;
  1         2  
  1         28  
45 1     1   608 use Finance::LocalBitcoins::API::Request::AdGet;
  1         2  
  1         29  
46 1     1   627 use Finance::LocalBitcoins::API::Request::AdsGet;
  1         2  
  1         28  
47 1     1   819 use Finance::LocalBitcoins::API::Request::AdUpdate;
  1         3  
  1         30  
48 1     1   823 use Finance::LocalBitcoins::API::Request::Ad;
  1         4  
  1         36  
49            
50 1     1   9 use constant COMPANY => 'LocalBitcoins';
  1         4  
  1         52  
51 1     1   6 use constant ERROR_NO_REQUEST => 'No request object to send';
  1         3  
  1         53  
52 1     1   5 use constant ERROR_NOT_READY => 'Not enough information to send a %s request';
  1         2  
  1         60  
53 1     1   5 use constant ERROR_IS_IT_READY => "The request is%s READY to send\n";
  1         2  
  1         60  
54 1     1   5 use constant ERROR_RESPONSE => COMPANY . ' error';
  1         2  
  1         77  
55 1     1   6 use constant ERROR_UNKNOWN_STATUS => COMPANY . " returned an unknown status\n";
  1         2  
  1         46  
56              
57 1     1   6 use constant ATTRIBUTES => qw(token);
  1         2  
  1         145  
58              
59 1         4061 use constant CLASS_ACTION_MAP => {
60             user => 'Finance::LocalBitcoins::API::Request::User',
61             me => 'Finance::LocalBitcoins::API::Request::Me',
62             pin => 'Finance::LocalBitcoins::API::Request::Pin',
63             dash => 'Finance::LocalBitcoins::API::Request::Dash',
64             release_escrow => 'Finance::LocalBitcoins::API::Request::ReleaseEscrow',
65             paid => 'Finance::LocalBitcoins::API::Request::Paid',
66             messages => 'Finance::LocalBitcoins::API::Request::Messages',
67             message => 'Finance::LocalBitcoins::API::Request::Message',
68             dispute => 'Finance::LocalBitcoins::API::Request::Dispute',
69             cancel => 'Finance::LocalBitcoins::API::Request::Cancel',
70             fund => 'Finance::LocalBitcoins::API::Request::Fund',
71             new_contact => 'Finance::LocalBitcoins::API::Request::NewContact',
72             contact => 'Finance::LocalBitcoins::API::Request::Contact',
73             contacts => 'Finance::LocalBitcoins::API::Request::Contacts',
74             wallet => 'Finance::LocalBitcoins::API::Request::Wallet',
75             balance => 'Finance::LocalBitcoins::API::Request::Balance',
76             'send' => 'Finance::LocalBitcoins::API::Request::Send',
77             sendpin => 'Finance::LocalBitcoins::API::Request::SendPin',
78             address => 'Finance::LocalBitcoins::API::Request::Address',
79             logout => 'Finance::LocalBitcoins::API::Request::Logout',
80             ads => 'Finance::LocalBitcoins::API::Request::Ads',
81             ad_get => 'Finance::LocalBitcoins::API::Request::AdGet',
82             ads_get => 'Finance::LocalBitcoins::API::Request::AdsGet',
83             ad_update => 'Finance::LocalBitcoins::API::Request::AdUpdate',
84             ad => 'Finance::LocalBitcoins::API::Request::Ad',
85             ticker => 'Finance::LocalBitcoins::API::Request::Ticker',
86             tradebook => 'Finance::LocalBitcoins::API::Request::TradeBook',
87             orderbook => 'Finance::LocalBitcoins::API::Request::OrderBook',
88 1     1   6 };
  1         2  
89              
90             sub is_ready_to_send {
91 10     10 0 24 my $self = shift;
92 10         22 my $ready = 0;
93             # here we are checking whether or not to default to '0' (not ready to send) based on this objects settings.
94             # the setting in here is the token provided to you by LocalBitcoins.
95             # if we dont have to add a token, then just check if its ready...
96 10 50 66     44 if (not $self->private or defined $self->token) {
97 10         28 $ready = $self->request->is_ready_to_send;
98             }
99 10         24 warn sprintf ERROR_IS_IT_READY, ($ready ? '' : ' NOT') if DEBUG;
100              
101 10         46 return $ready;
102             }
103              
104             sub send {
105 10     10 0 30 my $self = shift;
106              
107             # clear any previous response values... because if you wan it, you shoulda put a variable on it.
108 10         32 $self->response(undef);
109 10         38 $self->error(undef);
110              
111 10 50       34 unless ($self->request) {
112 0         0 $self->error({
113             type => __PACKAGE__,
114             message => ERROR_NO_REQUEST,
115             });
116             }
117             else {
118             # validate that the minimum required request attributes are set here.
119 10 50       47 if (not $self->is_ready_to_send) {
120 0         0 $self->error({
121             type => __PACKAGE__,
122             message => sprintf(ERROR_NOT_READY, ref $self->request),
123             });
124             }
125             else {
126             # make sure we have an request to send...
127 10         74 my $request = $self->http_request(HTTP::Request->new);
128 10         33 $request->method($self->request->request_type);
129 10         116 $request->uri($self->request->url);
130 10         12933 my %query_form = %{$self->request_content};
  10         42  
131             #
132             # This block will be removed once we have basic testing completed.
133             # ...because printing these variables on a live system is not a good idea...
134             #
135             #if ($self->private) {
136             # print Data::Dumper->Dump([\%query_form],['Query Form']);
137             # printf "Token: %s\n", $self->token;
138             # printf "Path: %s\n", $self->path;
139             #}
140             #
141 10 100       45 if ($self->private) {
142 7         21 $query_form{access_token} = $self->token;
143             }
144              
145 10         47 my $uri = URI->new;
146 10         511 $uri->query_form(%query_form);
147 10 100 66     841 if ($self->request->request_type eq 'POST') {
    100          
148 6         21 $request->content($uri->query);
149 6         226 $request->content_type($self->request->content_type);
150             }
151             elsif ($self->request->request_type eq 'GET' and $uri->query) {
152 2         35 $request->uri($request->uri . '?' . $uri->query);
153             }
154            
155 10         478 $request->header(Accept => 'application/json');
156              
157             # create a new user_agent each time...
158 10         807 $self->user_agent(LWP::UserAgent->new);
159 10         36 $self->user_agent->agent('Mozilla/8.0');
160 10         663 $self->user_agent->ssl_opts(verify_hostname => 1);
161              
162 10         282 warn Data::Dumper->Dump([$self->user_agent, $request],[qw(UserAgent Request)]) if DEBUG;
163              
164 10         39 $self->http_response($self->user_agent->request($request));
165 10         44 $self->process_response;
166             }
167             }
168 10         37 return $self->is_success;
169             }
170              
171             sub process_response {
172 10     10 0 23 my $self = shift;
173              
174 10         20 warn sprintf "Content: %s\n", $self->http_response->content if DEBUG;
175              
176             eval {
177 10         16 my $content;
178 10         21 warn Data::Dumper->Dump([$self->http_response],['Response']) if DEBUG;
179 10         43 $content = $self->json->decode($self->http_response->content);
180 0 0       0 if (ref $content eq 'ARRAY') {
    0          
    0          
181 0         0 $self->response($content);
182             }
183             elsif (exists $content->{error}) {
184 0         0 $self->error({
185             type => ERROR_RESPONSE,
186 0         0 %{$content->{error}},
187             });
188             }
189             elsif ($self->http_response->code != 200) {
190 0         0 warn sprintf "Invalid Server Response Code: %s\n", $self->http_response->code if VERBOSE;
191 0         0 $self->error({
192             type => 'Server Response Error',
193             message => sprintf('%s Server Response: %s', COMPANY, $self->http_response->code),
194             });
195             }
196             else {
197 0         0 $self->response($content);
198             }
199 0         0 1;
200 10 50       26 } or do {
201 10         1504 warn "eval error: $@\n";
202 10         87 $self->error({
203             type => 'eval/json error',
204             message => $@,
205             });
206             };
207              
208 10         49 return $self->is_success;
209             }
210              
211 2     2 1 2528 sub new { (bless {} => shift)->init(@_) }
212 0     0 0 0 sub path { URI->new(shift->http_request->uri)->path }
213 10     10 0 33 sub request_content { shift->request->request_content }
214 10   66 10 0 134 sub json { shift->{json} ||= JSON->new }
215 20     20 0 47 sub is_success { defined shift->response }
216 20     20 0 52 sub private { shift->request->is_private }
217 0     0 0 0 sub public { not shift->private }
218 2     2 0 8 sub attributes { ATTRIBUTES }
219              
220 1     1 0 1532 sub user { class_action(@_) }
221 1     1 0 1684 sub me { class_action(@_) }
222 1     1 0 1587 sub pin { class_action(@_) }
223 1     1 0 1582 sub dash { class_action(@_) }
224 0     0 0 0 sub release_escrow { class_action(@_) }
225 0     0 0 0 sub paid { class_action(@_) }
226 0     0 0 0 sub messages { class_action(@_) }
227 0     0 0 0 sub message { class_action(@_) }
228 0     0 0 0 sub dispute { class_action(@_) }
229 0     0 0 0 sub cancel { class_action(@_) }
230 0     0 0 0 sub fund { class_action(@_) }
231 0     0 0 0 sub new_contact { class_action(@_) }
232 0     0 0 0 sub contact { class_action(@_) }
233 0     0 0 0 sub contacts { class_action(@_) }
234 1     1 0 1577 sub wallet { class_action(@_) }
235 1     1 0 700 sub balance { class_action(@_) }
236 0     0 0 0 sub send_coin { class_action(@_) }
237 0     0 0 0 sub send_pin { class_action(@_) }
238 0     0 0 0 sub address { class_action(@_) }
239 0     0 0 0 sub logout { class_action(@_) }
240 1     1 0 7310 sub ads { class_action(@_) }
241 0     0 0 0 sub ad_get { class_action(@_) }
242 0     0 0 0 sub ads_get { class_action(@_) }
243 0     0 0 0 sub ad_update { class_action(@_) }
244 0     0 0 0 sub ad { class_action(@_) }
245 1     1 0 613 sub ticker { class_action(@_) }
246 1     1 0 2125 sub tradebook { class_action(@_) }
247 1     1 0 1555 sub orderbook { class_action(@_) }
248              
249 15     15 1 41 sub token { get_set(@_) }
250 30     30 1 82 sub error { get_set(@_) }
251 20     20 0 17413 sub http_response { get_set(@_) }
252 100     100 1 213 sub request { get_set(@_) }
253 30     30 0 69 sub response { get_set(@_) }
254 10     10 0 658 sub http_request { get_set(@_) }
255 40     40 1 5996 sub user_agent { get_set(@_) }
256              
257             sub init {
258 2     2 0 6 my $self = shift;
259 2         7 my %args = @_;
260 2         8 foreach my $attribute ($self->attributes) {
261 2 100       13 $self->$attribute($args{$attribute}) if exists $args{$attribute};
262             }
263 2         13 return $self;
264             }
265              
266             # this method simply makes all the get/setter attribute methods below very tidy...
267             sub get_set {
268 245     245 0 383 my $self = shift;
269 245         2454 my $attribute = ((caller(1))[3] =~ /::(\w+)$/)[0];
270 245 100       850 $self->{$attribute} = shift if scalar @_;
271 245         1793 return $self->{$attribute};
272             }
273              
274             sub class_action {
275 10     10 0 30 my $self = shift;
276 10         130 my $class = CLASS_ACTION_MAP->{((caller(1))[3] =~ /::(\w+)$/)[0]};
277 10         201 $self->request($class->new(@_));
278 10 50       45 return $self->send ? $self->response : undef;
279             }
280              
281             # These additional routines will allow you to easily encrypt your API secret using a similar but random text string as a key.
282             # Generate and store a random string of 40 hex chars in your script.
283             # perl -e 'use Finance::CaVirtex::API qw(string_encrypt); print "Encrypted: %s\n", string_encrypt('put your token here', $random_key);
284             # to output the cyphertext of the real secret encrypted using your key.
285             # Your script should then load the cyphertext from an external file and call this:
286             # my $api_secret = string_decrypt($cyphertext, $random_key);
287             # Since both the random_key and the cyphertext are in separate files, a breach would require both files to be compromised.
288             # If you also put the token into a database table that is accessed during runtime... then you are further protected.
289             # This setup would require 3 distinct components which would all need to be compromised to gain unwanted access to your API keys and functions.
290             #
291             # From the command line, you can generate a set of semi-random strings that should be good enough for this using:
292             # perl -e 'print join("",("a".."z",0..9)[map rand$_,(36) x 22])."\n"for 1..20;'
293             #
294             # select one of those as your random_key.
295             #
296              
297             # encryption works by assigning an ordinal value to each character '0' = 0 ... 'Z' = 35
298             # these values are then added for each character in the cypher and the random key.
299             # the modulus of the sum is then taken to remain within the 36 available characters.
300             # this number is then converted back to character.
301             # once each character of the string is calculated, the complete cyphertext is generated.
302             #
303             # the end result is that we are adding the secret string to the random key string to obtain the cyphertext:
304             # Cypher = Secret + Key
305             #
306             # decryption is exactly like encryption except we take the difference of each character
307             # instead of the sum.
308             #
309             # the end result is thatIn this way we are subtracting the random key from the cyphertext to get back the secret string.
310             # Secret = Cypher - Key
311             #
312             # I believe this method is equivalent to XOR encryption, which is very strong as long as the key is random and kept secret.
313             #
314 0 0   0 0   sub alphanum_to_digit { ord($_[0]) > 57 ? ord($_[0]) - 87 : ord($_[0]) - 48 }
315 0 0   0 0   sub digit_to_alphanum { chr($_[0] > 9 ? $_[0] + 87 : $_[0] + 48) }
316 0     0 0   sub string_encrypt { join '', map(digit_to_alphanum((alphanum_to_digit(substr $_[0], $_, 1) + alphanum_to_digit(substr $_[1], $_, 1)) % 36), 0 .. length($_[0]) - 1) }
317 0     0 0   sub string_decrypt { join '', map(digit_to_alphanum((alphanum_to_digit(substr $_[0], $_, 1) - alphanum_to_digit(substr $_[1], $_, 1)) % 36), 0 .. length($_[0]) - 1) }
318 0     0 0   sub gen_random_key { join("",("a".."f",0..9)[map rand$_,(16) x 40]) }
319              
320              
321             1;
322              
323             __END__