File Coverage

blib/lib/Steemit/WsClient.pm
Criterion Covered Total %
statement 61 133 45.8
branch 6 28 21.4
condition 1 16 6.2
subroutine 11 18 61.1
pod 3 4 75.0
total 82 199 41.2


line stmt bran cond sub pod time code
1             package Steemit::WsClient;
2              
3             =head1 NAME
4              
5             Steemit::WsClient - perl library for interacting with the steemit websocket services!
6              
7             =head1 VERSION
8              
9             Version 0.11
10              
11             =cut
12              
13             our $VERSION = '0.11';
14              
15              
16             =head1 SYNOPSIS
17              
18              
19             use Steemit::WsClient;
20              
21             my $foo = Steemit::WsClient->new();
22             my $steem = Steemit::WsClient->new( url => 'https://some.steemit.d.node.address');
23              
24             say "Initialized Steemit::WsClient client with url ".$steem->url;
25              
26             #get the last 99 discussions with the tag utopian-io
27             #truncate the body since we dont care here
28             my $discussions = $steem->get_discussions_by_created({
29             tag => 'utopian-io',
30             limit => 99,
31             truncate_body => 100,
32             });
33              
34             #extract the author names out of the result
35             my @author_names = map { $_->{author} } @$discussions;
36             say "last 99 authors: ".join(", ", @author_names);
37              
38             #load the author details
39             my $authors = $steem->get_accounts( [@author_names] );
40             #say Dumper $authors->[0];
41              
42             #calculate the reputation average
43             my $reputation_sum = 0;
44             for my $author ( @$authors ){
45             $reputation_sum += int( $author->{reputation} / 1000_000_000 );
46             }
47              
48             say "Average reputation of the last 99 utopian authors: ". ( int( $reputation_sum / scalar(@$authors) ) / 100 );
49              
50              
51             =head1 DEPENDENCIES
52              
53             you will need some packages.
54             openssl support for https
55             libgmp-dev for large integer aritmetic needd for the eliptical curve calculations
56              
57             libssl-dev zlib1g-dev libgmp-dev
58              
59              
60             =head1 SUBROUTINES/METHODS
61              
62             =cut
63              
64 3     3   5117 use Modern::Perl;
  3         18173  
  3         16  
65 3     3   1206 use Mojo::Base -base;
  3         203897  
  3         23  
66 3     3   2114 use Mojo::UserAgent;
  3         571685  
  3         35  
67 3     3   115 use Mojo::JSON qw(decode_json encode_json);
  3         7  
  3         128  
68 3     3   17 use Data::Dumper;
  3         5  
  3         1056  
69              
70             has url => 'https://api.steemit.com/';
71             has ua => sub { Mojo::UserAgent->new };
72             has posting_key => undef;
73             has plain_posting_key => \&_transform_private_key;
74              
75              
76             =head2 all database api methods of the steemit api
77              
78             L
79              
80             get_miner_queue
81             lookup_account_names
82             get_discussions
83             get_discussions_by_blog
84             get_witness_schedule
85             get_open_orders
86             get_trending_tags
87             lookup_witness_accounts
88             get_discussions_by_children
89             get_accounts
90             get_savings_withdraw_to
91             get_potential_signatures
92             get_required_signatures
93             get_order_book
94             get_key_references
95             get_tags_used_by_author
96             get_account_bandwidth
97             get_replies_by_last_update
98             get_dynamic_global_properties
99             get_block
100             get_witnesses
101             get_transaction_hex
102             get_comment_discussions_by_payout
103             get_discussions_by_votes
104             get_witness_by_account
105             verify_authority
106             get_config
107             get_account_votes
108             get_discussions_by_promoted
109             get_conversion_requests
110             get_account_history
111             get_escrow
112             get_discussions_by_comments
113             get_feed_history
114             get_hardfork_version
115             set_block_applied_callback
116             get_discussions_by_author_before_date
117             get_discussions_by_hot
118             get_discussions_by_payout
119             get_discussions_by_trending
120             get_recovery_request
121             get_reward_fund
122             get_chain_properties
123             get_witnesses_by_vote
124             get_account_references
125             get_post_discussions_by_payout
126             get_active_witnesses
127             get_ops_in_block
128             get_discussions_by_created
129             get_discussions_by_active
130             get_account_count
131             get_owner_history
132             get_next_scheduled_hardfork
133             get_savings_withdraw_from
134             get_active_votes
135             get_current_median_history_price
136             get_transaction
137             get_block_header
138             get_expiring_vesting_delegations
139             get_witness_count
140             get_content
141             verify_account_authority
142             get_liquidity_queue
143             get_discussions_by_feed
144             get_discussions_by_cashout
145             get_content_replies
146             lookup_accounts
147             get_state
148             get_withdraw_routes
149              
150              
151             =head2 get_discussions_by_xxxxxx
152              
153             all those methods will sort the results differently and accept one query parameter with the values:
154              
155             {
156             tag => 'tagtosearch', # optional
157             limit => 1, # max 100
158             filter_tags => [], # tags to filter out
159             select_authors => [], # only those authors
160             truncate_body => 0 # the number of bytes of the post body to return, 0 for all
161             start_author => '' # used together with the start_permlink gor pagination
162             start_permlink => '' #
163             parent_author => '' #
164             parent_permlink => '' #
165             }
166              
167             so one example on how to get 200 discussions would be
168              
169              
170             my $discussions = $steem->get_discussions_by_created({
171             limit => 100,
172             truncate_body => 1,
173             });
174              
175             my $discussion = $discussions[-1];
176              
177             push @$discussions, $steem->get_discussions_by_created({
178             limit => 100,
179             truncate_body => 1,
180             start_author => $discussion->{author},
181             start_permlink => $discussion->{permlink},
182             });
183              
184             =cut
185             sub _request {
186 0     0   0 my( $self, $api, $method, @params ) = @_;
187 0         0 my $response = $self->ua->post( $self->url, json => {
188             jsonrpc => '2.0',
189             method => 'call',
190             params => [$api,$method,[@params]],
191             id => int rand 100,
192             })->result;
193              
194 0 0       0 die "error while requesting steemd ". $response->to_string unless $response->is_success;
195              
196 0         0 my $result = decode_json $response->body;
197              
198 0 0       0 return $result->{result} if $result->{result};
199 0 0       0 if( my $error = $result->{error} ){
200 0         0 die $error->{message};
201             }
202             #ok no error no result
203 0         0 require Data::Dumper;
204 0         0 die "unexpected api result: ".Data::Dumper::Dumper( $result );
205             }
206              
207             _install_methods();
208              
209             sub _install_methods {
210 3     3   6 my %definition = _get_api_definition();
211 3         10 for my $api ( keys %definition ){
212 6         8 for my $method ( @{ $definition{$api} } ){
  6         13  
213 3     3   21 no strict 'subs';
  3         6  
  3         102  
214 3     3   14 no strict 'refs';
  3         13  
  3         4569  
215 207         725 my $package_sub = join '::', __PACKAGE__, $method;
216             *$package_sub = sub {
217 0     0   0 shift->_request($api,$method,@_);
218             }
219 207         1115 }
220             }
221             }
222              
223              
224             sub _get_api_definition {
225              
226 3     3   22 my @database_api = qw(
227             get_miner_queue
228             lookup_account_names
229             get_discussions
230             get_discussions_by_blog
231             get_witness_schedule
232             get_open_orders
233             get_trending_tags
234             lookup_witness_accounts
235             get_discussions_by_children
236             get_accounts
237             get_savings_withdraw_to
238             get_potential_signatures
239             get_required_signatures
240             get_order_book
241             get_tags_used_by_author
242             get_account_bandwidth
243             get_replies_by_last_update
244             get_dynamic_global_properties
245             get_block
246             get_witnesses
247             get_transaction_hex
248             get_comment_discussions_by_payout
249             get_discussions_by_votes
250             get_witness_by_account
251             verify_authority
252             get_config
253             get_account_votes
254             get_discussions_by_promoted
255             get_conversion_requests
256             get_account_history
257             get_escrow
258             get_discussions_by_comments
259             get_feed_history
260             get_hardfork_version
261             set_block_applied_callback
262             get_discussions_by_author_before_date
263             get_discussions_by_hot
264             get_discussions_by_payout
265             get_discussions_by_trending
266             get_recovery_request
267             get_reward_fund
268             get_chain_properties
269             get_witnesses_by_vote
270             get_account_references
271             get_post_discussions_by_payout
272             get_active_witnesses
273             get_ops_in_block
274             get_discussions_by_created
275             get_discussions_by_active
276             get_account_count
277             get_owner_history
278             get_next_scheduled_hardfork
279             get_savings_withdraw_from
280             get_active_votes
281             get_current_median_history_price
282             get_transaction
283             get_block_header
284             get_expiring_vesting_delegations
285             get_witness_count
286             get_content
287             verify_account_authority
288             get_liquidity_queue
289             get_discussions_by_feed
290             get_discussions_by_cashout
291             get_content_replies
292             lookup_accounts
293             get_state
294             get_withdraw_routes
295             );
296              
297             return (
298 3         28 database_api => [@database_api],
299             account_by_key_api => [ qw( get_key_references )],
300             )
301             }
302              
303              
304             =head2 vote
305              
306             this requires you to initialize the module with your private posting key like this:
307              
308              
309             my $steem = Steemit::WsClient->new(
310             posting_key => 'copy this one from the steemit site',
311              
312             );
313              
314             $steem->vote($discussion,$weight)
315              
316             weight is optional default is 10000 wich equals to 100%
317              
318              
319             =cut
320              
321              
322             sub vote {
323 0     0 1 0 my( $self, @discussions ) = @_;
324              
325 0         0 my $weight;
326 0 0       0 $weight = pop @discussions, unless ref $discussions[-1];
327 0   0     0 $weight = $weight // 10000;
328 0         0 my $voter = $self->get_key_references([$self->public_posting_key])->[0][0];
329              
330 0         0 my @operations = map { [
331             vote => {
332             voter => $voter,
333             author => $_->{author},
334             permlink => $_->{permlink},
335 0         0 weight => $weight,
336             }
337             ]
338             } @discussions;
339 0         0 return $self->_broadcast_transaction(@operations);
340             }
341              
342             =head2 comment
343              
344             this requires you to initialize the module with your private posting key like this:
345              
346              
347             my $steem = Steemit::WsClient->new(
348             posting_key => 'copy this one from the steemit site',
349              
350             );
351              
352             $steem->comment(
353             "parent_author" => $parent_author,
354             "parent_permlink" => $parent_permlink,
355             "author" => $author,
356             "permlink" => $permlink,
357             "title" => $title,
358             "body" => $body,
359             "json_metadata" => $json_metadata,
360             )
361              
362             you need at least a permlink and body
363             fill the parent parameters to comment on an existing post
364             json metadata can be already a json string or a perl hash
365              
366             =cut
367              
368             sub comment {
369 0     0 1 0 my( $self, %params ) = @_;
370              
371 0   0     0 my $parent_author = $params{parent_author} // '';
372 0   0     0 my $parent_permlink = $params{parent_permlink} // '';
373 0 0       0 my $permlink = $params{permlink} or die "permlink missing for comment";
374 0   0     0 my $title = $params{title} // '';
375 0 0       0 my $body = $params{body} or die "body missing for comment";
376              
377 0   0     0 my $json_metadata = $params{json_metadata} // {};
378 0 0       0 if( ref $json_metadata ){
379 0         0 $json_metadata = encode_json( $json_metadata);
380             }
381              
382 0         0 my $author = $self->get_key_references([$self->public_posting_key])->[0][0];
383              
384 0         0 my $operation = [
385             comment => {
386             "parent_author" => $parent_author,
387             "parent_permlink" => $parent_permlink,
388             "author" => $author,
389             "permlink" => $permlink,
390             "title" => $title,
391             "body" => $body,
392             "json_metadata" => $json_metadata,
393             }
394             ];
395 0         0 return $self->_broadcast_transaction($operation);
396             }
397              
398             =head2 delete_comment
399              
400             $steem->delete_comment(
401             author => $author,
402             permlink => $permlink
403             )
404              
405             you need the permlink
406             author will be filled with the user of your posting key if missing
407              
408             =cut
409              
410             sub delete_comment {
411 0     0 1 0 my( $self, %params ) = @_;
412              
413 0 0       0 my $permlink = $params{permlink} or die "permlink missing for comment";
414              
415 0   0     0 my $author = $params{author} // $self->get_key_references([$self->public_posting_key])->[0][0];
416              
417 0         0 my $operation = [
418             delete_comment => {
419             "author" => $author,
420             "permlink" => $permlink,
421             }
422             ];
423 0         0 return $self->_broadcast_transaction($operation);
424             }
425              
426              
427             sub _broadcast_transaction {
428 0     0   0 my( $self, @operations ) = @_;
429              
430 0         0 my $properties = $self->get_dynamic_global_properties();
431              
432 0         0 my $block_number = $properties->{last_irreversible_block_num};
433 0         0 my $block_details = $self->get_block( $block_number );
434              
435             my $ref_block_id = $block_details->{previous},
436              
437 0         0 my $time = $properties->{time};
438             #my $expiration = "2018-02-24T17:00:51";#TODO dynamic date
439 0         0 my ($year,$month,$day, $hour,$min,$sec) = split /\D/, $time;
440 0         0 require Date::Calc;
441 0         0 my $epoch = Date::Calc::Date_to_Time($year,$month,$day, $hour,$min,$sec);
442 0         0 ($year,$month,$day, $hour,$min,$sec) = Date::Calc::Time_to_Date($epoch + 600 );
443 0         0 my $expiration = "$year-$month-$day".'T'."$hour:$min:$sec";
444              
445 0         0 my $transaction = {
446             ref_block_num => ( $block_number - 1 )& 0xffff,
447             ref_block_prefix => unpack( "xxxxV", pack('H*',$ref_block_id)),
448             expiration => $expiration,
449             operations => [@operations],
450             extensions => [],
451             signatures => [],
452             };
453 0         0 my $serialized_transaction = $self->_serialize_transaction_message( $transaction );
454              
455 0         0 my $bin_private_key = $self->plain_posting_key;
456 0         0 require Steemit::ECDSA;
457 0         0 my ( $r, $s, $i ) = Steemit::ECDSA::ecdsa_sign( $serialized_transaction, Math::BigInt->from_bytes( $bin_private_key ) );
458 0         0 $i += 4;
459 0         0 $i += 27;
460              
461 0         0 my $signature = join('', map { unpack 'H*', $_ } ( pack("C", $i ), map { $_->as_bytes} ($r,$s )) );
  0         0  
  0         0  
462 0 0       0 unless( Steemit::ECDSA::is_signature_canonical_canonical( pack "H*", $signature ) ){
463 0         0 die "signature $signature is not canonical";
464             }
465              
466 0         0 $transaction->{signatures} = [ $signature ];
467              
468              
469 0         0 $self->_request('network_broadcast_api','broadcast_transaction_synchronous',$transaction);
470             }
471              
472             sub public_posting_key {
473 0     0 0 0 my( $self ) = @_;
474 0 0       0 unless( $self->{public_posting_key} ){
475 0         0 require Steemit::ECDSA;
476 0         0 my $bin_pubkey = Steemit::ECDSA::get_compressed_public_key( Math::BigInt->from_bytes( $self->plain_posting_key ) );
477             #TODO use the STM from dynamic lookup in get_config or somewhere
478 0         0 require Crypt::RIPEMD160;
479 0         0 my $rip = Crypt::RIPEMD160->new;
480 0         0 $rip->reset;
481 0         0 $rip->add($bin_pubkey);
482 0         0 my $checksum = $rip->digest;
483 0         0 $rip->reset;
484 0         0 $rip->add('');
485 0         0 $self->{public_posting_key} = "STM".Steemit::Base58::encode_base58($bin_pubkey.substr($checksum,0,4));
486             }
487              
488             return $self->{public_posting_key}
489 0         0 }
490              
491              
492             sub _transform_private_key {
493 3     3   2627 my( $self ) = @_;
494 3 50       20 die "posting_key missing" unless( $self->posting_key );
495              
496 3         20 my $base58 = $self->posting_key;
497              
498 3         486 require Steemit::Base58;
499 3         11 my $binary = Steemit::Base58::decode_base58( $base58 );
500              
501              
502 3         3786 my $version = substr( $binary, 0, 1 );
503 3         10 my $binary_private_key = substr( $binary, 1, -4);
504 3         7 my $checksum = substr( $binary, -4);
505 3 100       19 die "invalid version in wif ( 0x80 needed ) " unless $version eq pack "H*", '80';
506              
507 2         20 require Digest::SHA;
508 2         32 my $generated_checksum = substr( Digest::SHA::sha256( Digest::SHA::sha256( $version.$binary_private_key )), 0, 4 );
509              
510 2 100       25 die "invalid checksum " unless $generated_checksum eq $checksum;
511              
512 1         9 return $binary_private_key;
513             }
514              
515             sub _serialize_transaction_message {
516 1     1   811 my ($self,$transaction) = @_;
517              
518 1         14 my $serialized_transaction;
519              
520 1         3 $serialized_transaction .= pack 'v', $transaction->{ref_block_num};
521              
522 1         3 $serialized_transaction .= pack 'V', $transaction->{ref_block_prefix};
523              
524 1         359 require Date::Calc;
525             #2016-08-08T12:24:17
526 1         4305 my @dates = split /\D/, $transaction->{expiration} ;
527 1         7 my $epoch = Date::Calc::Date_to_Time( @dates);
528              
529 1         4 $serialized_transaction .= pack 'L', $epoch;
530              
531 1         2 $serialized_transaction .= pack "C", scalar( @{ $transaction->{operations} });
  1         3  
532              
533 1         390 require Steemit::OperationSerializer;
534 1         4 my $op_ser = Steemit::OperationSerializer->new;
535              
536 1         2 for my $operation ( @{ $transaction->{operations} } ) {
  1         3  
537              
538 1         2 my ($operation_name,$operations_parameters) = @$operation;
539 1         3 $serialized_transaction .= $op_ser->serialize_operation(
540             $operation_name,
541             $operations_parameters,
542             );
543             }
544              
545             #extentions in case we realy need them at some point we will have to implement this is a less nive way ;)
546 1 50 33     7 die "extentions not supported" if $transaction->{extensions} and $transaction->{extensions}[0];
547 1         2 $serialized_transaction .= pack 'H*', '00';
548              
549 1         11 return pack( 'H*', ( '0' x 64 )).$serialized_transaction;
550             }
551              
552              
553              
554              
555              
556              
557             =head1 REPOSITORY
558              
559             L
560              
561              
562             =head1 AUTHOR
563              
564             snkoehn, C<< >>
565              
566             =head1 BUGS
567              
568             Please report any bugs or feature requests to C, or through
569             the web interface at L. I will be notified, and then you'll
570             automatically be notified of progress on your bug as I make changes.
571              
572              
573              
574              
575             =head1 SUPPORT
576              
577             You can find documentation for this module with the perldoc command.
578              
579             perldoc Steemit::WsClient
580              
581              
582             You can also look for information at:
583              
584             =over 4
585              
586             =item * RT: CPAN's request tracker (report bugs here)
587              
588             L
589              
590             =item * AnnoCPAN: Annotated CPAN documentation
591              
592             L
593              
594             =item * CPAN Ratings
595              
596             L
597              
598             =item * Search CPAN
599              
600             L
601              
602             =back
603              
604              
605             =head1 ACKNOWLEDGEMENTS
606              
607              
608             =head1 LICENSE AND COPYRIGHT
609              
610             Copyright 2018 snkoehn.
611              
612             This program is free software; you can redistribute it and/or modify it
613             under the terms of the the Artistic License (2.0). You may obtain a
614             copy of the full license at:
615              
616             L
617              
618             Any use, modification, and distribution of the Standard or Modified
619             Versions is governed by this Artistic License. By using, modifying or
620             distributing the Package, you accept this license. Do not use, modify,
621             or distribute the Package, if you do not accept this license.
622              
623             If your Modified Version has been derived from a Modified Version made
624             by someone other than you, you are nevertheless required to ensure that
625             your Modified Version complies with the requirements of this license.
626              
627             This license does not grant you the right to use any trademark, service
628             mark, tradename, or logo of the Copyright Holder.
629              
630             This license includes the non-exclusive, worldwide, free-of-charge
631             patent license to make, have made, use, offer to sell, sell, import and
632             otherwise transfer the Package with respect to any patent claims
633             licensable by the Copyright Holder that are necessarily infringed by the
634             Package. If you institute patent litigation (including a cross-claim or
635             counterclaim) against any party alleging that the Package constitutes
636             direct or contributory patent infringement, then this Artistic License
637             to you shall terminate on the date that such litigation is filed.
638              
639             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
640             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
641             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
642             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
643             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
644             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
645             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
646             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
647              
648              
649             =cut
650              
651             1; # End of Steemit::WsClient