File Coverage

blib/lib/Business/Bitcoin/Request.pm
Criterion Covered Total %
statement 115 141 81.5
branch 24 54 44.4
condition 8 15 53.3
subroutine 21 23 91.3
pod 3 3 100.0
total 171 236 72.4


line stmt bran cond sub pod time code
1             # -*-cperl-*-
2             #
3             # Business::Bitcoin::Request - Bitcoin payment request
4             # Copyright (c) Ashish Gulhati
5             #
6             # $Id: lib/Business/Bitcoin/Request.pm v1.051 Tue Oct 16 22:26:58 PDT 2018 $
7              
8 2     2   11 use warnings;
  2         3  
  2         61  
9 2     2   9 use strict;
  2         3  
  2         47  
10              
11             package Business::Bitcoin::Request;
12              
13 2     2   8 use DBI;
  2         3  
  2         50  
14 2     2   1082 use LWP::UserAgent;
  2         78409  
  2         58  
15 2     2   12 use HTTP::Request;
  2         4  
  2         41  
16 2     2   1092 use Math::EllipticCurve::Prime;
  2         92297  
  2         63  
17 2     2   14 use Math::EllipticCurve::Prime::Point;
  2         4  
  2         61  
18 2     2   874 use Digest::SHA qw(sha256 sha256_hex hmac_sha512_hex);
  2         4092  
  2         155  
19 2     2   748 use Encode::Base58::BigInt;
  2         1755  
  2         93  
20 2     2   809 use Crypt::RIPEMD160;
  2         4663  
  2         80  
21              
22 2     2   12 use vars qw( $VERSION $AUTOLOAD );
  2         4  
  2         3165  
23              
24             our ( $VERSION ) = '$Revision: 1.051 $' =~ /\s+([\d\.]+)/;
25              
26             sub new {
27 2     2 1 10 my ($class, %args) = @_;
28 2 50 33     16 return undef if $args{Amount} !~ /^\d+$/; return undef if $args{StartIndex} and $args{StartIndex} =~ /\D/;
  2 50       10  
29 2         74 my $db = $args{_BizBTC}->db; my $xpub = $args{_BizBTC}->xpub;
  2         13  
30 2         6 my $timestamp = time;
31 2 100       6 my $index = defined $args{StartIndex} ? $args{StartIndex} : 'NULL';
32 2 100       7 my $refid = defined $args{Reference} ? "'$args{Reference}'" : 'NULL';
33 2 50       19 return undef unless $db->do("INSERT INTO requests values ($index, '$args{Amount}', NULL, $refid, '$timestamp', NULL, NULL);");
34 2         390 $index = $db->last_insert_id('%', '%', 'requests', 'reqid');
35 2         13 $ENV{PATH} = undef;
36 2 50       7 return undef unless my $address = _getaddress($args{_BizBTC}, $index);
37 2         48 my $rows = $db->do("UPDATE requests set address='$address' where reqid='$index';");
38             bless { Address => $address,
39             ID => $index,
40             Amount => $args{Amount},
41             DB => $db,
42             Reference => $args{Reference},
43 2 50       726 Confirmations => defined $args{Confirmations} ? $args{Confirmations} : 5,
44             Created => $timestamp }, $class;
45             }
46              
47             sub verify {
48 0     0 1 0 my $self = shift;
49 0         0 my $ua = new LWP::UserAgent;
50 0         0 my $req = HTTP::Request->new(GET => 'https://blockchain.info/q/addressbalance/' . $self->address . '?confirmations=' . $self->confirmations);
51 0         0 my $res = $ua->request($req);
52 0         0 my $paid = $res->content;
53 0 0       0 $self->error($paid), return if $paid =~ /\D/;
54 0         0 $self->error('');
55 0 0       0 $paid >= $self->amount ? $paid : 0;
56             }
57              
58             sub _find {
59 2     2   24 my ($class, %args) = @_;
60 2 50 66     15 return unless defined $args{Address} or defined $args{Reference};
61 2 50 66     12 return if defined $args{Address} and defined $args{Reference};
62 2 50 33     18 return unless defined $args{_BizBTC} and $args{_BizBTC}->db->ping;
63              
64             my $query = 'SELECT reqid,address,amount,reference,created,processed,status from requests WHERE ' .
65 2 100       56 (defined $args{Address} ? "address='$args{Address}';" : "reference='$args{Reference}';");
66 2         12 my ($reqid, $address, $amount, $refid, $created, $processed, $status) = $args{_BizBTC}->db->selectrow_array($query);
67             bless { Address => $address,
68             Amount => $amount,
69             Reference => $refid,
70             ID => $reqid,
71             DB => $args{_BizBTC}->db,,
72 2 50       347 Confirmations => defined $args{Confirmations} ? $args{Confirmations} : 5,
73             Created => $created,
74             Processed => $processed,
75             Status => $status
76             }, $class;
77             }
78              
79             sub commit {
80 0     0 1 0 my $self = shift;
81 0         0 my $processed = $self->processed;
82 0         0 my $status = $self->status;
83 0         0 my $amount = $self->amount;
84 0         0 my $refid = $self->reference;
85 0         0 my $address = $self->address;
86 0         0 my @updates;
87 0 0       0 push @updates, "processed = '" . $processed . "'" if $processed;
88 0 0       0 push @updates, "reference = '" . $refid . "'" if $refid;
89 0 0       0 push @updates, "status = '" . $status . "'" if $status;
90 0 0       0 push @updates, "amount = '" . $amount . "'" if $amount;
91 0 0       0 return 1 unless my $updates = join ',',@updates;
92 0 0       0 return undef unless $self->db->do("UPDATE requests SET $updates where address='$address';");
93 0         0 return 1;
94             }
95              
96             sub _getaddress {
97 2     2   8 my ($bizbtc, $index) = @_;
98 2         13 my $xpub = $bizbtc->xpub;
99 2         17 my $curve = Math::EllipticCurve::Prime->from_name('secp256k1');
100 2         8153 my $xpubdata = Math::BigInt->new(_decode58($xpub))->as_hex;
101 2         87776 $xpubdata =~ /.(.{8})(..)(.{8})(.{8})(.{64})(.{66})(.*)/;
102 2         18 my ($ver, $depth, $fp, $i, $c, $Kc) = ($1, $2, $3, $4, $5, $6);
103 2         10 my $K = Math::EllipticCurve::Prime::Point->from_hex(_decompress($Kc));
104 2 100       3962 if ($bizbtc->path eq 'electrum') {
105             # m/0
106 1         7 my ($Ki, $ci) = _CKDpub($K, $c, 0);
107             # m/0/$index
108 1         7 my ($Ki2, $ci2) = _CKDpub($Ki, $ci, $index);
109 1         5 return _address(_compress($Ki2));
110             }
111             else {
112 1         5 my ($Ki, $ci) = _CKDpub($K, $c, $index);
113 1         6 return _address(_compress($Ki));
114             }
115             }
116              
117             sub _CKDpub {
118 3     3   11 my ($K, $c, $i) = @_;
119 3         21 my $curve = Math::EllipticCurve::Prime->from_name('secp256k1');
120 3         12233 my $data = pack('H*', _compress($K)) . pack ('L>', $i);
121 3         706 my $hmac = hmac_sha512_hex($data, pack('H*', $c)); $hmac =~ /(.{64})(.{64})/;
  3         14  
122 3         13 my ($Il, $ci) = ($1, $2);
123 3         16 my $Ki = $curve->g->multiply(Math::BigInt->from_hex($Il))->add($K);
124 3         37801926 return ($Ki, $ci);
125             }
126              
127             sub _address {
128 2     2   507 my $sha256 = sha256(pack('H*', shift));
129 2         20 my $id = '00' . Crypt::RIPEMD160->hexhash($sha256); $id =~ s/\s//g;
  2         142  
130 2         24 my $checksum = substr(sha256_hex(sha256(pack('H*', $id))), 0, 8);
131 2         17 my $address = _encode58(Math::BigInt->from_hex($id . $checksum));
132 2         10 my $leadingones;
133 2         24 while ($id =~ /^(00)/) { $leadingones .= '1'; $id =~ s/^00//; }
  3         9  
  3         17  
134 2         51 return $leadingones . $address;
135             }
136              
137             sub _decompress {
138 2     2   4 my $Kc = shift; $Kc =~ /^(..)(.*)/;
  2         7  
139 2         7 my $i = $1; my $K = '04' . '0' x (64 - length($2)) . $2; my $x = Math::BigInt->from_hex($2);
  2         10  
  2         9  
140 2         1774 my $curve = Math::EllipticCurve::Prime->from_name('secp256k1');
141 2         7855 my ($p, $a, $b) = ($curve->p, $curve->a, $curve->b);
142 2         35 my $y = ($x->bmodpow(3,$p)+$a*$x+$b)->bmodpow(($p+1)/4,$p);
143 2 50       792324 $y = $p - $y if $i%2 ne $y%2;
144 2         951 my $yhex = $y->as_hex; $yhex =~ s/^0x//;
  2         1945  
145 2         10 $K .= '0' x (64 - length($yhex)) . $yhex;
146 2         21 return $K;
147             }
148              
149             sub _compress {
150 5     5   13 my $K = shift;
151 5         22 my $Kc = $K->x->as_hex; $Kc =~ s/^0x//;
  5         5016  
152 5         20 $Kc = '0' x (64 - length($Kc)) . $Kc;
153 5 100       22 $Kc = ($K->y % 2 ? '03' : '02') . $Kc;
154             }
155              
156             sub _decode58 {
157 2     2   4 my $todecode = shift;
158 2         6 $todecode =~ tr/A-HJ-NP-Za-km-z/a-km-zA-HJ-NP-Z/;
159 2         11 my $decoded = decode_base58($todecode);
160             }
161              
162             sub _encode58 {
163 2     2   1356 my $encoded = encode_base58(shift);
164 2         20075 $encoded =~ tr/a-km-zA-HJ-NP-Z/A-HJ-NP-Za-km-z/;
165 2         7 return $encoded;
166             }
167              
168             sub AUTOLOAD {
169 12     12   1180 my $self = shift; (my $auto = $AUTOLOAD) =~ s/.*:://;
  12         58  
170 12 100       137 return if $auto eq 'DESTROY';
171 8 50 66     40 if ($auto =~ /^(confirmations|processed|status|amount|reference|error)$/x and defined $_[0]) {
172 0         0 $self->{"\u$auto"} = shift;
173             }
174 8 50       60 if ($auto =~ /^(reqid|amount|address|reference|version|created|confirmations|processed|status|error)$/x) {
175 8         56 return $self->{"\u$auto"};
176             }
177 0 0         if ($auto =~ /^(db|id)$/x) {
178 0           return $self->{"\U$auto"};
179             }
180 0           die "Could not AUTOLOAD method $auto.";
181             }
182              
183             1; # End of Business::Bitcoin::Request
184              
185             =head1 NAME
186              
187             Business::Bitcoin::Request - Bitcoin payment request
188              
189             =head1 VERSION
190              
191             $Revision: 1.051 $
192             $Date: Tue Oct 16 22:26:58 PDT 2018 $
193              
194             =head1 SYNOPSIS
195              
196             Business::Bitcoin::Request objects represent Bitcoin payment requests
197             generated by Business::Bitcoin.
198              
199             use Business::Bitcoin;
200              
201             my $bizbtc = new Business::Bitcoin (DB => '/tmp/bizbtc.db',
202             XPUB => 'xpub...');
203              
204             my $request = $bizbtc->request(Amount => 4200);
205              
206             print ($request->verify ? "Verified\n" : "Verification failed\n");
207              
208             =head1 METHODS
209              
210             =head2 new
211              
212             Not intended to be called directly. Business::Bitcoin::Request objects
213             should be created by calling the request method on a Business::Bitcoin
214             object.
215              
216             =head2 commit
217              
218             Commit the Request object to the requests database. Only the
219             'processed' and 'status' fields are updated in the database.
220              
221             =head2 verify
222              
223             Verify that the request has been paid. Returns the total unspent
224             balance at the address corresponding to the request if the request has
225             been paid, and 0 if the balance at the address is lower than the
226             request amount. The number of confirmations required to consider a
227             payment valid can be set via the confirmations accessor.
228              
229             =head1 ACCESSORS
230              
231             Accessors can be called with no arguments to query the value of an
232             object property, or with a single argument, to set the property to a
233             specific value (unless the property is read only).
234              
235             =head2 confirmations
236              
237             The number of confirmations needed to consider a payment valid.
238              
239             =head2 amount
240              
241             The amount of the payment request, in Satoshi. Read only.
242              
243             =head2 address
244              
245             The Bitcoin receiving address for the payment request. Read only.
246              
247             =head2 created
248              
249             The timestamp of when the request was created. Stored as an int in the
250             requests database. Read only.
251              
252             =head2 reference
253              
254             An optional reference ID for the request, to facilitate integration
255             with existing order systems. Stored as a text field in the requests
256             database. Read only.
257              
258             =head2 processed
259              
260             An optional property for applications to record the timestamp of when
261             the transaction was processed. Stored as an int in the requests
262             database. Read/write.
263              
264             =head2 status
265              
266             An optional property that can be used to record the status of the
267             transaction ('processed', 'shipped', 'refunded', etc.). Stored as a
268             text field in the requests database. Read/write.
269              
270             =head2 error
271              
272             If the last verify() returned undef, this accessor will return the
273             error string that was received from the blockchain API call.
274              
275             =head2 version
276              
277             The version number of this module. Read only.
278              
279             =head1 AUTHOR
280              
281             Ashish Gulhati, C<< >>
282              
283             =head1 BUGS
284              
285             Please report any bugs or feature requests to C, or through
286             the web interface at L. I will be notified, and then you'll
287             automatically be notified of progress on your bug as I make changes.
288              
289             =head1 SUPPORT
290              
291             You can find documentation for this module with the perldoc command.
292              
293             perldoc Business::Bitcoin::Request
294              
295             You can also look for information at:
296              
297             =over 4
298              
299             =item * RT: CPAN's request tracker
300              
301             L
302              
303             =item * AnnoCPAN: Annotated CPAN documentation
304              
305             L
306              
307             =item * CPAN Ratings
308              
309             L
310              
311             =item * Search CPAN
312              
313             L
314              
315             =back
316              
317             =head1 LICENSE AND COPYRIGHT
318              
319             Copyright (c) Ashish Gulhati.
320              
321             This software package is Open Software; you can use, redistribute,
322             and/or modify it under the terms of the Open Artistic License 2.0.
323              
324             Please see L for the full license
325             terms, and ensure that the license grant applies to you before using
326             or modifying this software. By using or modifying this software, you
327             indicate your agreement with the license terms.