File Coverage

blib/lib/Web3/Tiny/ABI.pm
Criterion Covered Total %
statement 142 169 84.0
branch 54 88 61.3
condition 3 6 50.0
subroutine 26 29 89.6
pod 0 5 0.0
total 225 297 75.7


line stmt bran cond sub pod time code
1             package Web3::Tiny::ABI;
2              
3 2     2   101725 use strict;
  2         4  
  2         82  
4 2     2   11 use warnings;
  2         2  
  2         82  
5 2     2   8 use Exporter 'import';
  2         4  
  2         55  
6 2     2   1468 use Math::BigInt;
  2         38138  
  2         18  
7              
8 2     2   29705 use Web3::Tiny::Keccak256 qw(keccak256);
  2         12  
  2         4922  
9              
10             our $VERSION = '0.01';
11             our @EXPORT_OK = qw(
12             selector encode_call encode_types decode_result decode_types
13             );
14              
15             =head1 NAME
16              
17             Web3::Tiny::ABI - Solidity ABI encoding/decoding
18              
19             =head1 DESCRIPTION
20              
21             Supports the common scalar types (C, C, C
, C,
22             C, dynamic C/C) plus C and C arrays of
23             them. Does B support tuples/structs or arrays-of-arrays -- if you
24             need those, this is intentionally the "tiny" subset.
25              
26             Integers are accepted as plain Perl numbers, numeric strings, or
27             L objects, and are returned from C as
28             Math::BigInt objects (uint256 routinely exceeds native int range).
29             C
values are accepted/returned as C<"0x...">-prefixed 40-hex-char
30             strings. C/C values are accepted/returned as raw byte
31             strings unless given as a C<"0x...">-prefixed hex string, in which case
32             that convention is preserved on decode too... actually bytes/bytesN are
33             always *returned* as raw bytes; only C
round-trips as hex text.
34              
35             =cut
36              
37             # ------------------------------------------------------------------
38             # type introspection
39             # ------------------------------------------------------------------
40              
41             sub _is_dynamic_type {
42 34     34   62 my ($type) = @_;
43 34 100 66     140 return 1 if $type eq 'bytes' || $type eq 'string';
44 30 100       99 if ($type =~ /^(.+)\[(\d*)\]$/) {
45 4         22 my ($elem, $size) = ($1, $2);
46 4 50       20 return 1 if $size eq '';
47 0         0 return _is_dynamic_type($elem);
48             }
49 26         63 return 0;
50             }
51              
52             sub _static_size {
53 13     13   24 my ($type) = @_;
54 13 50       29 if ($type =~ /^(.+)\[(\d+)\]$/) {
55 0         0 return _static_size($1) * $2;
56             }
57 13         35 return 32;
58             }
59              
60             # ------------------------------------------------------------------
61             # scalar codecs
62             # ------------------------------------------------------------------
63              
64             sub _to_bigint {
65 8     8   15 my ($n) = @_;
66 8 50 33     19 return $n->copy if ref($n) && $n->isa('Math::BigInt');
67 8         46 return Math::BigInt->new("$n");
68             }
69              
70             sub _encode_uint_word {
71 8     8   40 my ($n) = @_;
72 8         64 my $bi = _to_bigint($n);
73 8 50       885 die "Web3::Tiny::ABI: negative value not valid for unsigned type\n" if $bi->is_neg;
74 8         71 my $hex = $bi->as_hex;
75 8         759 $hex =~ s/^0x//;
76 8 50       19 die "Web3::Tiny::ABI: integer too large for uint256\n" if length($hex) > 64;
77 8         27 $hex = ('0' x (64 - length($hex))) . $hex;
78 8         97 return pack('H*', $hex);
79             }
80              
81             sub _decode_uint_word {
82 7     7   16 my ($word) = @_;
83 7         42 return Math::BigInt->from_hex('0x' . unpack('H*', $word));
84             }
85              
86             sub _encode_int_word {
87 0     0   0 my ($n) = @_;
88 0         0 my $bi = _to_bigint($n);
89 0 0       0 if ($bi->is_neg) {
90 0         0 my $two_256 = Math::BigInt->new(2)->bpow(256);
91 0         0 $bi = $two_256->badd($bi);
92             }
93 0         0 return _encode_uint_word($bi);
94             }
95              
96             sub _decode_int_word {
97 0     0   0 my ($word) = @_;
98 0         0 my $v = _decode_uint_word($word);
99 0         0 my $two_255 = Math::BigInt->new(2)->bpow(255);
100 0 0       0 if ($v->bcmp($two_255) >= 0) {
101 0         0 my $two_256 = Math::BigInt->new(2)->bpow(256);
102 0         0 $v = $v->bsub($two_256);
103             }
104 0         0 return $v;
105             }
106              
107             sub _hexish_to_bytes {
108 2     2   5 my ($val) = @_;
109 2 50       7 return '' unless defined $val;
110 2 100       9 if ($val =~ /^0x([0-9a-fA-F]*)$/) {
111 1         3 my $hex = $1;
112 1 50       5 $hex = '0' . $hex if length($hex) % 2;
113 1         8 return pack('H*', $hex);
114             }
115 1         3 return $val;
116             }
117              
118             sub _encode_address {
119 1     1   3 my ($addr) = @_;
120 1         4 my $bytes = _hexish_to_bytes($addr);
121 1 50       3 die "Web3::Tiny::ABI: address must be 20 bytes\n" unless length($bytes) == 20;
122 1         6 return ("\x00" x 12) . $bytes;
123             }
124              
125             sub _decode_address {
126 1     1   4 my ($word) = @_;
127 1         8 return '0x' . unpack('H*', substr($word, 12, 20));
128             }
129              
130             sub _encode_bytesN {
131 1     1   5 my ($val, $n) = @_;
132 1         4 my $bytes = _hexish_to_bytes($val);
133 1 50       5 die "Web3::Tiny::ABI: bytes$n value must be exactly $n bytes\n" unless length($bytes) == $n;
134 1         6 return $bytes . ("\x00" x (32 - $n));
135             }
136              
137             sub _encode_bytes_dynamic {
138 1     1   2 my ($bytes) = @_;
139 1         19 my $len = length($bytes);
140 1         3 my $pad = (32 - ($len % 32)) % 32;
141 1         24 return _encode_uint_word($len) . $bytes . ("\x00" x $pad);
142             }
143              
144             # ------------------------------------------------------------------
145             # generic tuple (and by extension: array) encode/decode
146             #
147             # Offsets for dynamic values are always relative to the start of the
148             # *current* tuple's own data window -- whether that's the top-level
149             # call arguments, or an array's element region right after its length
150             # word. This keeps encode/decode of nested dynamic arrays consistent.
151             # ------------------------------------------------------------------
152              
153             sub _slot_sizes {
154 8     8   18 my ($types) = @_;
155 8 100       20 return [map { _is_dynamic_type($_) ? 32 : _static_size($_) } @$types];
  17         32  
156             }
157              
158             sub encode_types {
159 3     3 0 905 my ($types, $values) = @_;
160 3 50       9 die "Web3::Tiny::ABI: types/values count mismatch\n" unless @$types == @$values;
161              
162 3         9 my $slot_sizes = _slot_sizes($types);
163 3         5 my $head_size = 0;
164 3         10 $head_size += $_ for @$slot_sizes;
165              
166 3         6 my @head_parts;
167             my @tail_parts;
168 3         11 for my $i (0 .. $#$types) {
169 8         17 my $type = $types->[$i];
170 8         11 my $val = $values->[$i];
171 8 100       15 if (_is_dynamic_type($type)) {
172 2         9 push @head_parts, { dyn => 1, idx => scalar(@tail_parts) };
173 2         6 push @tail_parts, _encode_value($type, $val);
174             }
175             else {
176 6         16 push @head_parts, { raw => _encode_value($type, $val) };
177             }
178             }
179              
180 3         8 my @tail_offsets;
181 3         4 my $running = 0;
182 3         6 for my $tp (@tail_parts) {
183 2         5 push @tail_offsets, $running;
184 2         4 $running += length($tp);
185             }
186              
187 3         7 my $head_str = '';
188 3         4 for my $hp (@head_parts) {
189 8 100       21 if ($hp->{raw}) {
190 6         13 $head_str .= $hp->{raw};
191             }
192             else {
193 2         7 $head_str .= _encode_uint_word($head_size + $tail_offsets[$hp->{idx}]);
194             }
195             }
196              
197 3         30 return $head_str . join('', @tail_parts);
198             }
199              
200             sub _encode_value {
201 8     8   18 my ($type, $value) = @_;
202              
203 8 100       20 if ($type =~ /^(.+)\[(\d*)\]$/) {
204 1         4 my ($elem, $size) = ($1, $2);
205 1 50       6 my @vals = ref($value) eq 'ARRAY' ? @$value : ($value);
206 1 50       4 if ($size eq '') {
207 1         3 return _encode_uint_word(scalar @vals)
208             . encode_types([($elem) x scalar(@vals)], \@vals);
209             }
210 0 0       0 die "Web3::Tiny::ABI: expected $size elements for $type, got " . scalar(@vals) . "\n"
211             unless scalar(@vals) == $size;
212 0         0 return encode_types([($elem) x $size], \@vals);
213             }
214              
215 7 100       19 return _encode_bytes_dynamic($value) if $type eq 'bytes';
216 6 50       13 return _encode_bytes_dynamic(_utf8_bytes($value)) if $type eq 'string';
217 6 100       17 return _encode_address($value) if $type eq 'address';
218 5 0       9 return _encode_uint_word($value ? 1 : 0) if $type eq 'bool';
    50          
219 5 100       24 return _encode_uint_word($value) if $type =~ /^uint\d*$/;
220 1 50       5 return _encode_int_word($value) if $type =~ /^int\d*$/;
221 1 50       9 return _encode_bytesN($value, $1) if $type =~ /^bytes(\d+)$/;
222 0         0 die "Web3::Tiny::ABI: unsupported type '$type'\n";
223             }
224              
225             sub _utf8_bytes {
226 0     0   0 my ($str) = @_;
227 0 0       0 return '' unless defined $str;
228 0 0       0 return $str unless utf8::is_utf8($str);
229 0         0 require Encode;
230 0         0 return Encode::encode('UTF-8', $str);
231             }
232              
233             sub decode_types {
234 5     5 0 908 my ($types, $data) = @_;
235 5         12 my $slot_sizes = _slot_sizes($types);
236 5         10 my @slot_offsets;
237 5         9 my $running = 0;
238 5         10 for my $sz (@$slot_sizes) {
239 9         16 push @slot_offsets, $running;
240 9         16 $running += $sz;
241             }
242              
243 5         10 my @out;
244 5         16 for my $i (0 .. $#$types) {
245 9         997 push @out, _decode_value($types->[$i], $data, $slot_offsets[$i]);
246             }
247 5         313 return @out;
248             }
249              
250             sub _decode_value {
251 9     9   23 my ($type, $data, $head_offset) = @_;
252              
253 9 100       18 if (_is_dynamic_type($type)) {
254 2         9 my $rel = _decode_uint_word(substr($data, $head_offset, 32))->numify;
255 2         1089 return _decode_dynamic($type, substr($data, $rel));
256             }
257              
258 7         19 my $word = substr($data, $head_offset, 32);
259 7         18 return _decode_static($type, $word, $data, $head_offset);
260             }
261              
262             # static value living inline in the head (word already sliced out, but
263             # fixed-size arrays of static elements need the wider window too)
264             sub _decode_static {
265 7     7   18 my ($type, $word, $data, $head_offset) = @_;
266              
267 7 50       19 if ($type =~ /^(.+)\[(\d+)\]$/) {
268 0         0 my ($elem, $size) = ($1, $2);
269 0         0 my $window = substr($data, $head_offset, 32 * $size);
270 0         0 return [decode_types([($elem) x $size], $window)];
271             }
272              
273 7 100       26 return $word eq "\x00" x 32 ? 0 : 1 if $type eq 'bool';
    100          
274 5 100       18 return _decode_address($word) if $type eq 'address';
275 4 100       18 return _decode_uint_word($word) if $type =~ /^uint\d*$/;
276 1 50       5 return _decode_int_word($word) if $type =~ /^int\d*$/;
277 1 50       10 return substr($word, 0, $1) if $type =~ /^bytes(\d+)$/;
278 0         0 die "Web3::Tiny::ABI: unsupported type '$type'\n";
279             }
280              
281             # dynamic value: $data here is already windowed to start at the value's
282             # own offset (i.e. $data =~ /^length . content/ for bytes/string/array)
283             sub _decode_dynamic {
284 2     2   8 my ($type, $data) = @_;
285              
286 2 100       13 if ($type =~ /^(.+)\[\]$/) {
287 1         3 my $elem = $1;
288 1         4 my $count = _decode_uint_word(substr($data, 0, 32))->numify;
289 1         298 return [decode_types([($elem) x $count], substr($data, 32))];
290             }
291              
292 1         4 my $len = _decode_uint_word(substr($data, 0, 32))->numify;
293 1         289 return substr($data, 32, $len);
294             }
295              
296             # ------------------------------------------------------------------
297             # function-call level helpers
298             # ------------------------------------------------------------------
299              
300             sub _parse_signature {
301 1     1   3 my ($sig) = @_;
302 1 50       15 die "Web3::Tiny::ABI: bad signature '$sig'\n"
303             unless $sig =~ /^\s*(\w+)\s*\(\s*(.*?)\s*\)\s*$/;
304 1         7 my ($name, $arglist) = ($1, $2);
305 1 50       11 my @types = length($arglist) ? split(/\s*,\s*/, $arglist) : ();
306 1         4 return ($name, \@types);
307             }
308              
309             # selector($sig) -> 4 raw bytes
310             sub selector {
311 3     3 0 237422 my ($sig) = @_;
312 3         15 return substr(keccak256($sig), 0, 4);
313             }
314              
315             # encode_call($sig, @args) -> raw call data (selector . encoded args)
316             sub encode_call {
317 1     1 0 986 my ($sig, @args) = @_;
318 1         22 my (undef, $types) = _parse_signature($sig);
319 1 50       5 die "Web3::Tiny::ABI: '$sig' expects " . scalar(@$types) . " args, got " . scalar(@args) . "\n"
320             unless scalar(@$types) == scalar(@args);
321 1         3 return selector($sig) . encode_types($types, \@args);
322             }
323              
324             # decode_result(\@types, $raw_bytes) -> list of decoded values
325             sub decode_result {
326 3     3 0 3877 my ($types, $data) = @_;
327 3         9 return decode_types($types, $data);
328             }
329              
330             1;