File Coverage

blib/lib/Ethereum/RPC/Contract.pm
Criterion Covered Total %
statement 36 147 24.4
branch 0 36 0.0
condition 0 34 0.0
subroutine 12 26 46.1
pod 9 9 100.0
total 57 252 22.6


line stmt bran cond sub pod time code
1             package Ethereum::RPC::Contract;
2             # ABSTRACT: Support for interacting with Ethereum contracts using the geth RPC interface
3              
4 4     4   29 use strict;
  4         14  
  4         133  
5 4     4   34 use warnings;
  4         12  
  4         206  
6              
7             our $VERSION = '0.04';
8              
9             =head1 NAME
10              
11             Ethereum::Contract - Support for interacting with Ethereum contracts using the geth RPC interface
12              
13             =cut
14              
15 4     4   36 use Moo;
  4         78  
  4         52  
16 4     4   1620 use JSON::MaybeXS;
  4         9  
  4         292  
17 4     4   4442 use Math::BigInt;
  4         106423  
  4         26  
18 4     4   77143 use Scalar::Util qw(looks_like_number);
  4         10  
  4         251  
19 4     4   58 use List::Util qw(first);
  4         10  
  4         273  
20 4     4   1954 use Digest::Keccak qw(keccak_256_hex);
  4         3279  
  4         376  
21              
22 4     4   34 use Ethereum::RPC::Client;
  4         9  
  4         142  
23 4     4   1842 use Ethereum::RPC::Contract::ContractResponse;
  4         68  
  4         168  
24 4     4   2138 use Ethereum::RPC::Contract::ContractTransaction;
  4         13  
  4         146  
25 4     4   29 use Ethereum::RPC::Contract::Helper::UnitConversion;
  4         15  
  4         8463  
26              
27             has contract_address => (is => 'rw');
28             has contract_abi => (
29             is => 'ro',
30             required => 1
31             );
32             has rpc_client => (
33             is => 'lazy',
34             );
35              
36             sub _build_rpc_client {
37 0     0     return Ethereum::RPC::Client->new;
38             }
39              
40             has from => (
41             is => 'rw',
42             lazy => 1
43             );
44              
45             sub _build_from {
46 0     0     return shift->rpc_client->eth_coinbase();
47             }
48              
49             has gas_price => (
50             is => 'rw',
51             lazy => 1
52             );
53              
54             sub _build_gas_price {
55 0     0     return shift->rpc_client->eth_gasPrice();
56             }
57              
58             has max_fee_per_gas => (is => 'rw');
59              
60             has max_priority_fee_per_gas => (is => 'rw');
61              
62             has gas => (is => 'rw');
63              
64             has contract_decoded => (
65             is => 'rw',
66             default => sub { {} },
67             );
68              
69             =head2 BUILD
70              
71             Constructor: Here we get all functions and events from the given ABI and set
72             it to the contract class.
73              
74             =over 4
75              
76             =item contract_address => string (optional)
77              
78             =item contract_abi => string (required, https://solidity.readthedocs.io/en/develop/abi-spec.html)
79              
80             =item rpc_client => L (optional, default: L)
81              
82             =item from => string (optional)
83              
84             =item gas => numeric (optional)
85              
86             =item gas_price => numeric (optional)
87              
88             =item max_fee_per_gas => numeric (optional)
89              
90             =item max_priority_fee_per_gas => numeric (optional)
91              
92             =back
93              
94             =cut
95              
96             sub BUILD {
97 0     0 1   my ($self) = @_;
98 0   0       my @decoded_json = @{decode_json($self->contract_abi // "[]")};
  0            
99              
100 0           for my $json_input (@decoded_json) {
101 0 0         if ($json_input->{type} =~ /^function|event|constructor$/) {
102 0   0       push(@{$self->contract_decoded->{$json_input->{name} // $json_input->{type}}}, $json_input->{inputs});
  0            
103             }
104             }
105              
106 0 0         unless ($self->contract_decoded->{constructor}) {
107 0           push(@{$self->contract_decoded->{constructor}}, []);
  0            
108             }
109              
110 0           return;
111              
112             }
113              
114             =head2 invoke
115              
116             Prepare a function to be called/sent to a contract.
117              
118             =over 4
119              
120             =item name => string (required)
121              
122             =item params => array (optional, the function params)
123              
124             =back
125              
126             Returns a L object.
127              
128             =cut
129              
130             sub invoke {
131 0     0 1   my ($self, $name, @params) = @_;
132              
133 0           my $function_id = substr($self->get_function_id($name, scalar @params), 0, 10);
134              
135 0           my $res = $self->_prepare_transaction($function_id, $name, \@params);
136              
137 0           return $res;
138             }
139              
140             =head2 get_function_id
141              
142             The function ID is derived from the function signature using: SHA3(approve(address,uint256)).
143              
144             =over 4
145              
146             =item fuction_name => string (required)
147              
148             =item params_size => numeric (required, size of inputs called by the function)
149              
150             =back
151              
152             Returns a string hash
153              
154             =cut
155              
156             sub get_function_id {
157 0     0 1   my ($self, $function_name, $params_size) = @_;
158              
159 0           my @inputs = @{$self->contract_decoded->{$function_name}};
  0            
160              
161 0 0 0 0     my $selected_data = first { (not $_ and not $params_size) or ($params_size and scalar @{$_} == $params_size) } @inputs;
  0   0        
  0            
162              
163 0           $function_name .= sprintf("(%s)", join(",", map { $_->{type} } grep { $_->{type} } @$selected_data));
  0            
  0            
164              
165 0           my $sha3_hex_function = '0x' . keccak_256_hex($function_name);
166              
167 0           return $sha3_hex_function;
168             }
169              
170             =head2 _prepare_transaction
171              
172             Join the data and parameters and return a prepared transaction to be called as send, call or deploy.
173              
174             =over 4
175              
176             =item compiled_data => string (required, function signature or the contract bytecode)
177              
178             =item function_name => string (contract function as specified in the ABI)
179              
180             =item params => array (required)
181              
182             =back
183              
184             L object
185             on_done: L
186             on_fail: error string
187              
188             =cut
189              
190             sub _prepare_transaction {
191 0     0     my ($self, $compiled_data, $function_name, $params) = @_;
192 0           $compiled_data =~ s/\s+//g;
193              
194 0           my $encoded = $self->encode($function_name, $params);
195              
196 0           my $data = $compiled_data . $encoded;
197              
198 0           my $transaction = Ethereum::RPC::Contract::ContractTransaction->new(
199             contract_address => $self->contract_address,
200             rpc_client => $self->rpc_client,
201             data => $self->append_prefix($data),
202             from => $self->from,
203             gas => $self->gas
204             );
205              
206 0 0         if ($self->gas_price) {
207 0           $transaction->{gas_price} = $self->gas_price;
208             # if the gas price is set the transaction type is legacy
209 0           return $transaction;
210             }
211              
212             # transaction type 2 EIP1559
213 0 0         $transaction->{max_fee_per_gas} = $self->max_fee_per_gas if $self->max_fee_per_gas;
214 0 0         $transaction->{max_priority_fee_per_gas} = $self->max_priority_fee_per_gas if $self->max_priority_fee_per_gas;
215 0           return $transaction;
216             }
217              
218             =head2 encode
219              
220             Encode function arguments to the ABI format
221              
222             =over 4
223              
224             =item C ABI function name
225              
226             =item C all the values for the function in the same order than the ABI
227              
228             =back
229              
230             Returns an encoded data string
231              
232             =cut
233              
234             sub encode {
235 0     0 1   my ($self, $function_name, $params) = @_;
236              
237 0           my $inputs = $self->contract_decoded->{$function_name}->[0];
238              
239             # no inputs
240 0 0         return "" unless $inputs;
241              
242 0           my $offset = $self->get_function_offset($inputs);
243              
244 0           my (@static, @dynamic);
245 0           my @inputs = $inputs->@*;
246 0           for (my $input_index = 0; $input_index < scalar @inputs; $input_index++) {
247 0           my ($static, $dynamic) = $self->get_hex_param($offset, $inputs[$input_index]->{type}, $params->[$input_index]);
248 0           push(@static, $static->@*);
249 0           push(@dynamic, $dynamic->@*);
250 0           $offset += scalar $dynamic->@*;
251             }
252              
253 0           my @data = (@static, @dynamic);
254 0           my $data = join("", @data);
255              
256 0           return $data;
257             }
258              
259             =head2 get_function_offset
260              
261             Get the abi function total offset
262              
263             For the cases we have arrays as parameters we can have a dynamic size
264             for the static values, for sample if the basic type has a fixed value
265             and also the array is fixed, we will have all the items on the array
266             being added with the static items before the dynamic items in the encoded
267             data
268              
269             =over 4
270              
271             =item C the json input from the abi data
272              
273             =back
274              
275             return the integer offset
276              
277             =cut
278              
279             sub get_function_offset {
280 0     0 1   my ($self, $input_list) = @_;
281 0           my $offset = 0;
282 0           for my $input ($input_list->@*) {
283 0           $input->{type} =~ /^([a-z]+)([0-9]+)?\[(\d+)?\]/;
284 0           my $basic_type = $1;
285 0           my $input_size = $2;
286 0           my $array_size = $3;
287 0 0 0       if ($input_size && $array_size || ($array_size && $basic_type =~ /^uint|int|fixed/)) {
      0        
      0        
288 0           $offset += $array_size;
289 0           next;
290             }
291 0           $offset += 1;
292             }
293 0           return $offset;
294             }
295              
296             =head2 get_hex_param
297              
298             Convert parameter list to the ABI format:
299             https://solidity.readthedocs.io/en/develop/abi-spec.html#function-selector-and-argument-encoding
300              
301             =over 4
302              
303             =item C The offset where we should base the calculation for the next dynamic value
304              
305             =item C The input type specified in the abi sample: string, bytes, uint
306              
307             =item C The input value
308              
309             =back
310              
311             Returns 2 arrays
312              
313             Static => contains the static values from the conversion
314             Dynamic => contains the dynamic values from the conversion
315              
316             =cut
317              
318             sub get_hex_param {
319 0     0 1   my ($self, $current_offset_count, $input_type, $param) = @_;
320              
321 0           my @static;
322             my @dynamic;
323              
324             # is an array
325 0 0 0       if ($input_type =~ /(\d+)?\[(\d+)?\]/) {
    0 0        
    0          
    0          
326 0           my $size = $param->@*;
327 0           my $static_item_size = $1;
328 0           my $static_array_size = $2;
329              
330             # if it is dynamic array we just write the offset
331 0 0 0       unless ($static_array_size && $static_item_size) {
332 0           push(@static, sprintf("%064s", Math::BigInt->new($current_offset_count * 32)->to_hex));
333             }
334              
335             # if the array is static we add the array size to the dynamic list
336 0 0         unless ($static_array_size) {
337 0           push(@dynamic, sprintf("%064s", Math::BigInt->new($size)->to_hex));
338             }
339              
340 0           my @internal_static;
341             my @internal_dynamic;
342              
343             # for each item on the array we call get_hex_param recursively
344             # passing the basic type with the size if it is present
345 0           $input_type =~ /^([a-z]+([0-9]+)?)\[(?:\d+)?\]/;
346 0           for my $item ($param->@*) {
347 0           my ($internal_static, $internal_dynamic) = $self->get_hex_param($size, $1, $item);
348 0           push(@internal_static, $internal_static->@*);
349 0           push(@internal_dynamic, $internal_dynamic->@*);
350             # the size of the array is used to calculate the current offset
351             # the static offset has already been calculated counting the params
352             # size, so we need to add now just the dynamic values
353 0           $size += $internal_dynamic->@*;
354             }
355              
356             # if the byte and the basic type have a fixed size
357             # they are considered static, so we can just add them to
358             # the static list
359 0 0 0       if ($static_item_size && $static_array_size) {
360 0           push(@static, @internal_static);
361             } else {
362 0           push(@dynamic, @internal_static);
363             }
364 0           push(@dynamic, @internal_dynamic);
365              
366             } elsif ($input_type eq 'address' && $param =~ /^0x[0-9A-F]+$/i) {
367 0           push(@static, sprintf("%064s", substr($param, 2)));
368             } elsif ($input_type =~ /^(u)?(int|bool)(\d+)?/ && looks_like_number($param)) {
369 0           push(@static, sprintf("%064s", Math::BigInt->new($param)->to_hex));
370             } elsif ($input_type =~ /^(?:string|bytes)(\d+)?$/) {
371 0           my $basic_type_size = $1;
372 0           my $hex_value;
373             my $size;
374             # is already an hexadecimal value
375 0 0         if ($param =~ /^(?:0x|0X)([a-fA-F0-9]+)$/) {
376             # hex without 0x
377 0           $hex_value = $1;
378 0           $size = length(pack("H*", $hex_value));
379             } else {
380 0           $hex_value = unpack("H*", $param);
381 0           $size = length($param);
382             }
383             # if it has a fixed size we can add the value directly
384             # this is mostly for the bytes
385 0 0         if ($basic_type_size) {
386 0           push(@static, $hex_value . "0" x (64 - length($hex_value)));
387             } else {
388 0           push(@static, sprintf("%064s", Math::BigInt->new($current_offset_count * 32)->to_hex));
389 0           push(@dynamic, sprintf("%064s", sprintf("%x", $size)));
390 0           push(@dynamic, $hex_value . "0" x (64 - length($hex_value)));
391             }
392             }
393              
394 0           return \@static, \@dynamic;
395              
396             }
397              
398             =head2 read_event
399              
400             Read the specified log from the specified block to the latest block
401              
402             =over 4
403              
404             =item from_block => numeric (optional)
405              
406             =item event => string (required)
407              
408             =item event_params_size => numeric (required)
409              
410             =back
411              
412             Returns a json encoded object: https://github.com/ethereum/wiki/wiki/JSON-RPC#returns-42
413              
414             =cut
415              
416             sub read_event {
417 0     0 1   my ($self, $from_block, $event, $event_params_size) = @_;
418              
419 0           my $function_id = $self->get_function_id($event, $event_params_size);
420              
421 0   0       $from_block = $self->append_prefix(unpack("H*", $from_block // "latest"));
422              
423 0           my $res = $self->rpc_client->eth_getLogs([{
424             address => $self->contract_address,
425             fromBlock => $from_block,
426             topics => [$function_id]}]);
427              
428 0           return $res;
429             }
430              
431             =head2 invoke_deploy
432              
433             Prepare a deploy transaction.
434              
435             =over 4
436              
437             =item compiled (required, contract bytecode)
438              
439             =item params (required, constructor params)
440              
441             =back
442              
443             Returns a L object.
444              
445             =cut
446              
447             sub invoke_deploy {
448 0     0 1   my ($self, $compiled_data, @params) = @_;
449 0           return $self->_prepare_transaction($compiled_data, "constructor", \@params);
450             }
451              
452             =head2 append_prefix
453              
454             Ensure that the given hexadecimal string starts with 0x.
455              
456             =over 4
457              
458             =item str => string (hexadecimal)
459              
460             =back
461              
462             Returns a string hexadecimal
463              
464             =cut
465              
466             sub append_prefix {
467 0     0 1   my ($self, $str) = @_;
468 0 0         return $str =~ /^0x/ ? $str : "0x$str";
469             }
470              
471             1;