File Coverage

blib/lib/Web3/Tiny/Wallet.pm
Criterion Covered Total %
statement 57 58 98.2
branch 13 22 59.0
condition 7 14 50.0
subroutine 11 11 100.0
pod 0 4 0.0
total 88 109 80.7


line stmt bran cond sub pod time code
1             package Web3::Tiny::Wallet;
2              
3 2     2   101200 use strict;
  2         4  
  2         60  
4 2     2   13 use warnings;
  2         2  
  2         91  
5              
6 2     2   712 use Web3::Tiny::Secp256k1 qw(privkey_to_pubkey pubkey_to_address sign_hash);
  2         6  
  2         139  
7 2     2   13 use Web3::Tiny::Keccak256 qw(keccak256);
  2         3  
  2         85  
8 2     2   885 use Web3::Tiny::RLP qw(rlp_encode int_to_bytes);
  2         6  
  2         1706  
9              
10             our $VERSION = '0.01';
11              
12             sub _hex_to_bytes {
13 6     6   11 my ($val) = @_;
14 6 50 33     31 return '' unless defined $val && length($val);
15 6 50       37 if ($val =~ /^0x([0-9a-fA-F]*)$/) {
16 6         16 my $hex = $1;
17 6 50       14 $hex = '0' . $hex if length($hex) % 2;
18 6         29 return pack('H*', $hex);
19             }
20 0         0 return $val;
21             }
22              
23             sub _strip_leading_zeros {
24 2     2   7 my ($bytes) = @_;
25 2         8 $bytes =~ s/^\x00+//;
26 2         9 return $bytes;
27             }
28              
29             # Web3::Tiny::Wallet->new(private_key => '0x...' or 32 raw bytes)
30             sub new {
31 1     1 0 637 my ($class, %opts) = @_;
32 1 50       6 die "Web3::Tiny::Wallet: 'private_key' is required\n" unless defined $opts{private_key};
33              
34 1         5 my $priv = _hex_to_bytes($opts{private_key});
35 1 50       5 die "Web3::Tiny::Wallet: private_key must be 32 bytes\n" unless length($priv) == 32;
36              
37 1         7 my ($x, $y) = privkey_to_pubkey($priv);
38 1         7 my $addr = pubkey_to_address($x, $y);
39              
40 1         16 return bless {
41             priv => $priv,
42             pub_x => $x,
43             pub_y => $y,
44             address => $addr,
45             }, $class;
46             }
47              
48             # address() -> "0x" + EIP-55 checksummed 40-hex-char address
49             sub address {
50 2     2 0 21 my ($self) = @_;
51 2         21 return to_checksum_address($self->{address});
52             }
53              
54             # to_checksum_address($addr) -- accepts "0x..." hex or 20 raw bytes
55             sub to_checksum_address {
56 6     6 0 165440 my ($addr) = @_;
57 6 100       29 my $bytes = (length($addr) == 20) ? $addr : _hex_to_bytes($addr);
58 6         22 my $hex = lc unpack('H*', $bytes);
59 6         23 my $hash = unpack('H*', keccak256($hex));
60              
61 6         96 my $out = '';
62 6         22 for my $i (0 .. length($hex) - 1) {
63 240         263 my $c = substr($hex, $i, 1);
64 240 100 100     473 if ($c =~ /[a-f]/ && hex(substr($hash, $i, 1)) >= 8) {
65 59         71 $out .= uc $c;
66             }
67             else {
68 181         185 $out .= $c;
69             }
70             }
71 6         42 return '0x' . $out;
72             }
73              
74             # sign_transaction(%tx) -> "0x..." raw signed legacy (EIP-155) transaction
75             #
76             # Required: nonce, gas_price, gas, chain_id
77             # Optional: to (omit/empty for contract creation), value (default 0),
78             # data (default ''; raw bytes or "0x..." hex)
79             #
80             # Pass large numeric fields (gas_price, value) as decimal strings or
81             # Math::BigInt objects if they might exceed ~2^53 -- Perl numeric
82             # literals like 1e18 are floats and will silently lose precision.
83             sub sign_transaction {
84 1     1 0 15 my ($self, %tx) = @_;
85              
86 1         5 for my $req (qw(nonce gas_price gas chain_id)) {
87 4 50       14 die "Web3::Tiny::Wallet: '$req' is required\n" unless defined $tx{$req};
88             }
89              
90 1 50 33     13 my $to_bytes = defined($tx{to}) && length($tx{to}) ? _hex_to_bytes($tx{to}) : '';
91 1 50 33     6 die "Web3::Tiny::Wallet: 'to' must be 20 bytes\n"
92             if length($to_bytes) && length($to_bytes) != 20;
93              
94 1 50       3 my $data = defined($tx{data}) ? _hex_to_bytes($tx{data}) : '';
95 1   50     3 my $value = $tx{value} // 0;
96              
97             my @unsigned = (
98             int_to_bytes($tx{nonce}),
99             int_to_bytes($tx{gas_price}),
100             int_to_bytes($tx{gas}),
101             $to_bytes,
102             int_to_bytes($value),
103             $data,
104 1         6 int_to_bytes($tx{chain_id}),
105             '',
106             '',
107             );
108              
109 1         3 my $hash = keccak256(rlp_encode(\@unsigned));
110 1         17 my ($r, $s, $recid) = sign_hash($hash, $self->{priv});
111 1         7 my $v = $tx{chain_id} * 2 + 35 + $recid;
112              
113             my @signed = (
114             int_to_bytes($tx{nonce}),
115             int_to_bytes($tx{gas_price}),
116 1         11 int_to_bytes($tx{gas}),
117             $to_bytes,
118             int_to_bytes($value),
119             $data,
120             int_to_bytes($v),
121             _strip_leading_zeros($r),
122             _strip_leading_zeros($s),
123             );
124              
125 1         5 return '0x' . unpack('H*', rlp_encode(\@signed));
126             }
127              
128             1;
129              
130             __END__