abicoder 0.0.1 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45fa5adeeafa44036fa386f4ab36d3f822e5f91706423d578ea822e0d6fc01a8
4
- data.tar.gz: 8430f2cdfeed351cee319596152b3c6d23ae506d6a51823ac909175df264eaff
3
+ metadata.gz: 3bc1e75e6080a55ae3447f7cfd3630ed587edf021658dd9159584fbb3ef28743
4
+ data.tar.gz: 84d27e90d278d0c2fc84db605005735164b4a63c084c087777218eadadb0044b
5
5
  SHA512:
6
- metadata.gz: 4bb46c9aa7376a6d3ff7a0ed76b51c60b6068e9705ed8b7ce2980d89dc1a38e7331ffd3681e616264138d7f0d9819cf7981d4a26c323a7fef0ec42792c956c9e
7
- data.tar.gz: 97ea5904ab15a98eea02f3a62d123c674bd34145828d04b93853dc615cf07031556143b6f51d4fa484a1b33479d26c7a6a1916acbb2df6dd503d4629e1dacac8
6
+ metadata.gz: 597f54c3c8e21027bb10d326c7c6763653d752ad3285abc1c2086023d17c6dc9207d8e99f16a008265bff3a2ad61e1f261db501911c46df56a509660102dc019
7
+ data.tar.gz: b776e32b5afd57e233aef6ffb87dba66b65272533f60f635c371e0a199fad593a1bd55cd6248f39a3002849b66e77ecf1bb7bd06f3ccc453cc87854d0c77d059
data/Manifest.txt CHANGED
@@ -3,4 +3,8 @@ Manifest.txt
3
3
  README.md
4
4
  Rakefile
5
5
  lib/abicoder.rb
6
+ lib/abicoder/decoder.rb
7
+ lib/abicoder/encoder.rb
8
+ lib/abicoder/parser.rb
9
+ lib/abicoder/types.rb
6
10
  lib/abicoder/version.rb
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Application Binary Inteface (ABI) Coder For Ethereum & Co.
2
2
 
3
- abicoder - application binary interface (abi) encoding / decoding machinery / helper for Ethereum & Co. (blockchain) contracts
3
+ abicoder - "lite" application binary interface (abi) encoding / decoding machinery / helper for Ethereum & Co. (blockchain) contracts with zero-dependencies for easy (re)use
4
4
 
5
5
 
6
6
  * home :: [github.com/rubycocos/blockchain](https://github.com/rubycocos/blockchain)
@@ -12,7 +12,136 @@ abicoder - application binary interface (abi) encoding / decoding machinery / he
12
12
 
13
13
  ## Usage
14
14
 
15
- To be done
15
+
16
+ ### Encode & Decode Types (Contract Function Call Data)
17
+
18
+
19
+ ``` ruby
20
+ require 'abicode'
21
+
22
+ #####
23
+ # try ABI.encode
24
+
25
+ ## Encoding simple types
26
+ types = [ 'uint256', 'string' ]
27
+ args = [ 1234, 'Hello World' ]
28
+ ABI.encode( types, args ) # returns binary blob / string
29
+ #=> hex"00000000000000000000000000000000000000000000000000000000000004d2"+
30
+ # "0000000000000000000000000000000000000000000000000000000000000040"+
31
+ # "000000000000000000000000000000000000000000000000000000000000000b"+
32
+ # "48656c6c6f20576f726c64000000000000000000000000000000000000000000"
33
+
34
+ ## Encoding with arrays types
35
+ types = [ 'uint256[]', 'string' ]
36
+ args = [ [1234, 5678] , 'Hello World' ]
37
+ ABI.encode( types, args ) # returns binary blob / string
38
+ #=> hex"0000000000000000000000000000000000000000000000000000000000000040"+
39
+ # "00000000000000000000000000000000000000000000000000000000000000a0"+
40
+ # "0000000000000000000000000000000000000000000000000000000000000002"+
41
+ # "00000000000000000000000000000000000000000000000000000000000004d2"+
42
+ # "000000000000000000000000000000000000000000000000000000000000162e"+
43
+ # "000000000000000000000000000000000000000000000000000000000000000b"+
44
+ # "48656c6c6f20576f726c64000000000000000000000000000000000000000000"
45
+
46
+ ## Encoding complex structs (also known as tuples)
47
+ types = [ 'uint256', '(uint256,string)']
48
+ args = [1234, [5678, 'Hello World']]
49
+ ABI.encode( types, args ) # returns binary blob / string
50
+ #=> hex'00000000000000000000000000000000000000000000000000000000000004d2'+
51
+ # '0000000000000000000000000000000000000000000000000000000000000040'+
52
+ # '000000000000000000000000000000000000000000000000000000000000162e'+
53
+ # '0000000000000000000000000000000000000000000000000000000000000040'+
54
+ # '000000000000000000000000000000000000000000000000000000000000000b'+
55
+ # '48656c6c6f20576f726c64000000000000000000000000000000000000000000'
56
+
57
+
58
+ #####
59
+ # try ABI.decode
60
+
61
+ ## Decoding simple types
62
+ types = [ 'uint256', 'string' ]
63
+ data = hex'00000000000000000000000000000000000000000000000000000000000004d2'+
64
+ '0000000000000000000000000000000000000000000000000000000000000040'+
65
+ '000000000000000000000000000000000000000000000000000000000000000b'+
66
+ '48656c6c6f20576f726c64000000000000000000000000000000000000000000'
67
+ ABI.decode( types, data) # returns args
68
+ #=> [1234, "Hello World"]
69
+
70
+
71
+ ## Decoding with arrays types
72
+ types = [ 'uint256[]', 'string' ]
73
+ data = hex'0000000000000000000000000000000000000000000000000000000000000040'+
74
+ '00000000000000000000000000000000000000000000000000000000000000a0'+
75
+ '0000000000000000000000000000000000000000000000000000000000000002'+
76
+ '00000000000000000000000000000000000000000000000000000000000004d2'+
77
+ '000000000000000000000000000000000000000000000000000000000000162e'+
78
+ '000000000000000000000000000000000000000000000000000000000000000b'+
79
+ '48656c6c6f20576f726c64000000000000000000000000000000000000000000'
80
+ ABI.decode( types, data ) # returns args
81
+ #=> [[1234, 5678], "Hello World"]
82
+
83
+
84
+ ## Decoding complex structs (also known as tuples)
85
+ types = [ 'uint256', '(uint256,string)']
86
+ data = hex'00000000000000000000000000000000000000000000000000000000000004d2'+
87
+ '0000000000000000000000000000000000000000000000000000000000000040'+
88
+ '000000000000000000000000000000000000000000000000000000000000162e'+
89
+ '0000000000000000000000000000000000000000000000000000000000000040'+
90
+ '000000000000000000000000000000000000000000000000000000000000000b'+
91
+ '48656c6c6f20576f726c64000000000000000000000000000000000000000000'
92
+ ABI.decode( types, data ) # returns args
93
+ #=> [1234, [5678, "Hello World"]]
94
+ ```
95
+
96
+ and so on.
97
+
98
+
99
+
100
+
101
+
102
+ ## Reference
103
+
104
+ For the full (and latest) official application
105
+ binary inteface (abi) specification
106
+ see the [**Contract ABI Specification**](https://docs.soliditylang.org/en/develop/abi-spec.html).
107
+
108
+
109
+ ### Types
110
+
111
+ The following elementary types are supported:
112
+
113
+ - `uint<M>`: unsigned integer type of `M` bits, `0 < M <= 256`, `M % 8 == 0`. e.g. `uint32`, `uint8`, `uint256`.
114
+ - `int<M>`: two's complement signed integer type of `M` bits, `0 < M <= 256`, `M % 8 == 0`.
115
+ - `address`: equivalent to `uint160`, except for the assumed interpretation and language typing.
116
+ For computing the function selector, `address` is used.
117
+ - `bool`: equivalent to `uint8` restricted to the values 0 and 1. For computing the function selector, `bool` is used.
118
+ - `bytes<M>`: binary type of `M` bytes, `0 < M <= 32`.
119
+
120
+
121
+ The following (fixed-size) array types are supported:
122
+
123
+ - `<type>[M]`: a fixed-length array of `M` elements, `M >= 0`, of the given type.
124
+
125
+ <!--
126
+ Note: While this ABI specification can express fixed-length arrays with zero elements,
127
+ they're not supported by the compiler.
128
+ -->
129
+
130
+ The following non-fixed-size types are supported:
131
+
132
+ - `bytes`: dynamic sized byte sequence.
133
+ - `string`: dynamic sized unicode string assumed to be UTF-8 encoded.
134
+ - `<type>[]`: a variable-length array of elements of the given type.
135
+
136
+ Types can be combined to a tuple by enclosing them inside parentheses, separated by commas:
137
+
138
+ - `(T1,T2,...,Tn)`: tuple consisting of the types `T1`, ..., `Tn`, `n >= 0`
139
+
140
+ It is possible to form tuples of tuples, arrays of tuples and so on.
141
+
142
+ <!--
143
+ It is also possible to form zero-tuples (where `n == 0`).
144
+ -->
16
145
 
17
146
 
18
147
 
data/Rakefile CHANGED
@@ -2,11 +2,18 @@ require 'hoe'
2
2
  require './lib/abicoder/version.rb'
3
3
 
4
4
 
5
+ ###
6
+ # hack/ quick fix for broken intuit_values - overwrite with dummy
7
+ class Hoe
8
+ def intuit_values( input ); end
9
+ end
10
+
11
+
5
12
  Hoe.spec 'abicoder' do
6
13
 
7
14
  self.version = ABICoder::VERSION
8
15
 
9
- self.summary = "abicoder - application binary interface (abi) encoding / decoding machinery / helper for Ethereum & Co. (blockchain) contracts"
16
+ self.summary = "abicoder - 'lite' application binary interface (abi) encoding / decoding machinery / helper for Ethereum & Co. (blockchain) contracts with zero-dependencies for easy (re)use"
10
17
  self.description = summary
11
18
 
12
19
  self.urls = { home: 'https://github.com/rubycocos/blockchain' }
@@ -0,0 +1,208 @@
1
+ module ABI
2
+
3
+
4
+ class Decoder
5
+
6
+ ##
7
+ # Decodes multiple arguments using the head/tail mechanism.
8
+ #
9
+ def decode( types, data, raise_errors = false )
10
+ ## for convenience check if types is a String
11
+ ## otherwise assume ABI::Type already
12
+ types = types.map { |type| type.is_a?( Type ) ? type : Type.parse( type ) }
13
+
14
+ outputs = [nil] * types.size
15
+ start_positions = [nil] * types.size + [data.size]
16
+
17
+ # TODO: refactor, a reverse iteration will be better
18
+ pos = 0
19
+ types.each_with_index do |t, i|
20
+ # If a type is static, grab the data directly, otherwise record its
21
+ # start position
22
+ if t.dynamic?
23
+
24
+ if pos>data.size-1
25
+ if raise_errors
26
+ raise DecodingError, "Position out of bounds #{pos}>#{data.size-1}"
27
+ else
28
+ puts "!! WARN - DecodingError: Position out of bounds #{pos}>#{data.size-1}"
29
+ end
30
+ end
31
+
32
+ start_positions[i] = big_endian_to_int(data[pos, 32])
33
+
34
+ if start_positions[i]>data.size-1
35
+ if raise_errors
36
+ raise DecodingError, "Start position out of bounds #{start_positions[i]}>#{data.size-1}"
37
+ else
38
+ puts "!! WARN - DecodingError: Start position out of bounds #{start_positions[i]}>#{data.size-1}"
39
+ end
40
+ end
41
+
42
+
43
+ j = i - 1
44
+ while j >= 0 && start_positions[j].nil?
45
+ start_positions[j] = start_positions[i]
46
+ j -= 1
47
+ end
48
+
49
+ pos += 32
50
+ else
51
+ ## puts "step 1 - decode item [#{i}] - #{t.format} size: #{t.size} dynamic? #{t.dynamic?}"
52
+
53
+ outputs[i] = zero_padding( data, pos, t.size, start_positions )
54
+ pos += t.size
55
+ end
56
+ end
57
+
58
+
59
+
60
+ # We add a start position equal to the length of the entire data for
61
+ # convenience.
62
+ j = types.size - 1
63
+ while j >= 0 && start_positions[j].nil?
64
+ start_positions[j] = start_positions[types.size]
65
+ j -= 1
66
+ end
67
+
68
+ if pos > data.size
69
+ if raise_errors
70
+ raise DecodingError, "Not enough data for head"
71
+ else
72
+ puts "!! WARN - DecodingError: Not enough data for head"
73
+ end
74
+ end
75
+
76
+
77
+ types.each_with_index do |t, i|
78
+ if t.dynamic?
79
+ offset, next_offset = start_positions[i, 2]
80
+ if offset<=data.size && next_offset<=data.size
81
+ outputs[i] = data[offset...next_offset]
82
+ end
83
+ end
84
+ end
85
+
86
+ if outputs.include?(nil)
87
+ if raise_errors
88
+ raise DecodingError, "Not all data can be parsed"
89
+ else
90
+ puts "!! WARN: DecodingError - Not all data can be parsed"
91
+ end
92
+ end
93
+
94
+
95
+ types.zip(outputs).map do |(t, out)|
96
+ ## puts "step 2 - decode item - #{t.format} got: #{out.size} byte(s) - size: #{t.size} dynamic? #{t.dynamic?}"
97
+
98
+ decode_type(t, out)
99
+ end
100
+ end
101
+
102
+
103
+
104
+ def decode_type( type, arg )
105
+ return nil if arg.nil? || arg.empty?
106
+
107
+
108
+ if type.is_a?( String ) || type.is_a?( Bytes )
109
+ l = big_endian_to_int( arg[0,32] )
110
+ data = arg[32..-1]
111
+ data[0, l]
112
+ elsif type.is_a?( Tuple )
113
+ arg ? decode(type.types, arg) : []
114
+ elsif type.is_a?( FixedArray ) # static-sized arrays
115
+ l = type.dim
116
+ subtype = type.subtype
117
+ if subtype.dynamic?
118
+ start_positions = (0...l).map {|i| big_endian_to_int(arg[32*i, 32]) }
119
+ start_positions.push arg.size
120
+
121
+ outputs = (0...l).map {|i| arg[start_positions[i]...start_positions[i+1]] }
122
+
123
+ outputs.map {|out| decode_type(subtype, out) }
124
+ else
125
+ (0...l).map {|i| decode_type(subtype, arg[subtype.size*i, subtype.size]) }
126
+ end
127
+ elsif type.is_a?( Array )
128
+ l = big_endian_to_int( arg[0,32] )
129
+ raise DecodingError, "Too long length: #{l}" if l > 100000
130
+ subtype = type.subtype
131
+
132
+ if subtype.dynamic?
133
+ raise DecodingError, "Not enough data for head" unless arg.size >= 32 + 32*l
134
+
135
+ start_positions = (1..l).map {|i| 32+big_endian_to_int(arg[32*i, 32]) }
136
+ start_positions.push arg.size
137
+
138
+ outputs = (0...l).map {|i| arg[start_positions[i]...start_positions[i+1]] }
139
+
140
+ outputs.map {|out| decode_type(subtype, out) }
141
+ else
142
+ (0...l).map {|i| decode_type(subtype, arg[32 + subtype.size*i, subtype.size]) }
143
+ end
144
+ else
145
+ decode_primitive_type( type, arg )
146
+ end
147
+ end
148
+
149
+
150
+ def decode_primitive_type(type, data)
151
+ case type
152
+ when Address
153
+ encode_hex( data[12..-1] )
154
+ when String, Bytes
155
+ if data.length == 32
156
+ data[0..32]
157
+ else
158
+ size = big_endian_to_int( data[0,32] )
159
+ data[32..-1][0,size]
160
+ end
161
+ when FixedBytes
162
+ data[0, type.length]
163
+ when Uint
164
+ big_endian_to_int( data )
165
+ when Int
166
+ u = big_endian_to_int( data )
167
+ u >= 2**(type.bits-1) ? (u - 2**type.bits) : u
168
+ when Bool
169
+ data[-1] == BYTE_ONE
170
+ else
171
+ raise DecodingError, "Unknown primitive type: #{type.class.name} #{type.format}"
172
+ end
173
+ end
174
+
175
+
176
+ def zero_padding( data, pos, count, start_positions )
177
+ if pos >= data.size
178
+ start_positions[start_positions.size-1] += count
179
+ "\x00"*count
180
+ elsif pos + count > data.size
181
+ start_positions[start_positions.size-1] += ( count - (data.size-pos))
182
+ data[pos,data.size-pos] + "\x00"*( count - (data.size-pos))
183
+ else
184
+ data[pos, count]
185
+ end
186
+ end
187
+
188
+
189
+ ###########
190
+ # decoding helpers / utils
191
+
192
+ def big_endian_to_int( bin )
193
+ bin = bin.sub( /\A(\x00)+/, '' ) ## keep "performance" shortcut - why? why not?
194
+ ### todo/check - allow nil - why? why not?
195
+ ## raise DeserializationError, "Invalid serialization (not minimal length)" if !@size && serial.size > 0 && serial[0] == BYTE_ZERO
196
+ bin = bin || BYTE_ZERO
197
+ bin.unpack("H*").first.to_i(16)
198
+ end
199
+
200
+ def encode_hex( bin ) ## bin_to_hex
201
+ raise TypeError, "Value must be a String" unless bin.is_a?(::String)
202
+ bin.unpack("H*").first
203
+ end
204
+
205
+
206
+ end # class Decoder
207
+
208
+ end ## module ABI
@@ -0,0 +1,279 @@
1
+
2
+ module ABI
3
+ ##
4
+ # Contract ABI encoding and decoding.
5
+ #
6
+ # @see https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI
7
+ #
8
+
9
+ class EncodingError < StandardError; end
10
+ class DecodingError < StandardError; end
11
+ class ValueError < StandardError; end
12
+
13
+ class ValueOutOfBounds < ValueError; end
14
+
15
+
16
+
17
+ class Encoder
18
+
19
+ ##
20
+ # Encodes multiple arguments using the head/tail mechanism.
21
+ #
22
+ def encode( types, args )
23
+
24
+ ## todo/fix: enforce args.size and types.size must match!!
25
+ ## raise ArgumentError - expected x arguments got y!!!
26
+
27
+
28
+ ## for convenience check if types is a String
29
+ ## otherwise assume ABI::Type already
30
+ types = types.map { |type| type.is_a?( Type ) ? type : Type.parse( type ) }
31
+
32
+ ## todo/check: use args.map (instead of types)
33
+ ## might allow encoding less args than types? - why? why not?
34
+ head_size = types
35
+ .map {|type| type.size || 32 }
36
+ .sum
37
+
38
+ head, tail = '', ''
39
+ types.each_with_index do |type, i|
40
+ if type.dynamic?
41
+ head += encode_uint256( head_size + tail.size )
42
+ tail += encode_type( type, args[i] )
43
+ else
44
+ head += encode_type( type, args[i] )
45
+ end
46
+ end
47
+
48
+ head + tail
49
+ end
50
+
51
+ ##
52
+ # Encodes a single value (static or dynamic).
53
+ #
54
+ # @param type [ABI::Type] value type
55
+ # @param arg [Object] value
56
+ #
57
+ # @return [String] encoded bytes
58
+ #
59
+ def encode_type( type, arg )
60
+ ## case 1) string or bytes (note:are dynamic too!!! most go first)
61
+ ## use type == Type.new( 'string', nil, [] ) - same as Type.new('string'))
62
+ ## or type == Type.new( 'bytes', nil, [] ) - same as Type.new('bytes')
63
+ ## - why? why not?
64
+ if type.is_a?( String )
65
+ encode_string( arg )
66
+ elsif type.is_a?( Bytes )
67
+ encode_bytes( arg )
68
+ elsif type.is_a?( Tuple )
69
+ encode_tuple( type, arg )
70
+ elsif type.is_a?( Array ) || type.is_a?( FixedArray )
71
+ if type.dynamic?
72
+ encode_dynamic_array( type, arg )
73
+ else
74
+ encode_static_array( type, arg )
75
+ end
76
+ else # assume static (primitive) type
77
+ encode_primitive_type( type, arg )
78
+ end
79
+ end
80
+
81
+
82
+ def encode_dynamic_array( type, args )
83
+ raise ArgumentError, "arg must be an array" unless args.is_a?(::Array)
84
+
85
+ head, tail = '', ''
86
+
87
+ if type.is_a?( Array ) ## dynamic array
88
+ head += encode_uint256( args.size )
89
+ else ## fixed array
90
+ raise ArgumentError, "Wrong array size: found #{args.size}, expecting #{type.dim}" unless args.size == type.dim
91
+ end
92
+
93
+ subtype = type.subtype
94
+ args.each do |arg|
95
+ if subtype.dynamic?
96
+ head += encode_uint256( 32*args.size + tail.size )
97
+ tail += encode_type( subtype, arg )
98
+ else
99
+ head += encode_type( subtype, arg )
100
+ end
101
+ end
102
+ head + tail
103
+ end
104
+
105
+
106
+ def encode_static_array( type, args )
107
+ raise ArgumentError, "arg must be an array" unless args.is_a?(::Array)
108
+ raise ArgumentError, "Wrong array size: found #{args.size}, expecting #{type.dim}" unless args.size == type.dim
109
+
110
+ args.map {|arg| encode_type( type.subtype, arg ) }.join
111
+ end
112
+
113
+
114
+ ##
115
+ ## todo/check: if static tuple gets encoded different
116
+ ## without offset (head/tail)
117
+
118
+ def encode_tuple( tuple, args )
119
+ raise ArgumentError, "arg must be an array" unless args.is_a?(::Array)
120
+ raise ArgumentError, "Wrong array size (for tuple): found #{args.size}, expecting #{tuple.type.size} tuple elements" unless args.size == tuple.types.size
121
+
122
+ head_size = tuple.types
123
+ .map {|type| type.size || 32 }
124
+ .sum
125
+
126
+ head, tail = '', ''
127
+ tuple.types.each_with_index do |type, i|
128
+ if type.dynamic?
129
+ head += encode_uint256( head_size + tail.size )
130
+ tail += encode_type( type, args[i] )
131
+ else
132
+ head += encode_type( type, args[i] )
133
+ end
134
+ end
135
+
136
+ head + tail
137
+ end
138
+
139
+
140
+
141
+ def encode_primitive_type( type, arg )
142
+ case type
143
+ when Uint
144
+ ## note: for now size in bits always required
145
+ encode_uint( arg, type.bits )
146
+ when Int
147
+ ## note: for now size in bits always required
148
+ encode_int( arg, type.bits )
149
+ when Bool
150
+ encode_bool( arg )
151
+ when String
152
+ encode_string( arg )
153
+ when FixedBytes
154
+ encode_bytes( arg, type.length )
155
+ when Bytes
156
+ encode_bytes( arg )
157
+ when Address
158
+ encode_address( arg )
159
+ else
160
+ raise EncodingError, "Unhandled type: #{type.class.name} #{type.format}"
161
+ end
162
+ end
163
+
164
+
165
+
166
+ def encode_bool( arg )
167
+ raise ArgumentError, "arg is not bool: #{arg}" unless arg.is_a?(TrueClass) || arg.is_a?(FalseClass)
168
+ lpad_int( arg ? 1 : 0 )
169
+ end
170
+
171
+
172
+ def encode_uint256( arg ) encode_uint( arg, 256 ); end
173
+ def encode_uint( arg, bits )
174
+ raise ArgumentError, "arg is not integer: #{arg}" unless arg.is_a?(Integer)
175
+ raise ValueOutOfBounds, arg unless arg >= 0 && arg < 2**bits
176
+ lpad_int( arg )
177
+ end
178
+
179
+ def encode_int( arg, bits )
180
+ raise ArgumentError, "arg is not integer: #{arg}" unless arg.is_a?(Integer)
181
+ raise ValueOutOfBounds, arg unless arg >= -2**(bits-1) && arg < 2**(bits-1)
182
+ lpad_int( arg % 2**bits )
183
+ end
184
+
185
+
186
+ def encode_string( arg )
187
+
188
+ ## todo/fix: do NOT auto-unpack hexstring EVER!!!
189
+ ## remove arg.unpack('U*')
190
+
191
+ if arg.encoding == Encoding::UTF_8 ## was: name == 'UTF-8'
192
+ arg = arg.b
193
+ else
194
+ begin
195
+ arg.unpack('U*')
196
+ rescue ArgumentError
197
+ raise ValueError, "string must be UTF-8 encoded"
198
+ end
199
+ end
200
+
201
+ raise ValueOutOfBounds, "Integer invalid or out of range: #{arg.size}" if arg.size > UINT_MAX
202
+ size = lpad_int( arg.size )
203
+ value = rpad( arg, ceil32(arg.size) )
204
+
205
+ size + value
206
+ end
207
+
208
+
209
+ def encode_bytes( arg, length=nil )
210
+ raise EncodingError, "Expecting string: #{arg}" unless arg.is_a?(::String)
211
+ arg = arg.b
212
+
213
+ if length # fixed length type
214
+ raise ValueOutOfBounds, "invalid bytes length #{length}" if arg.size > length
215
+ raise ValueOutOfBounds, "invalid bytes length #{length}" if length < 0 || length > 32
216
+ rpad( arg )
217
+ else # variable length type (if length is nil)
218
+ raise ValueOutOfBounds, "Integer invalid or out of range: #{arg.size}" if arg.size > UINT_MAX
219
+ size = lpad_int( arg.size )
220
+ value = rpad( arg, ceil32(arg.size) )
221
+ size + value
222
+ end
223
+ end
224
+
225
+
226
+ def encode_address( arg )
227
+ if arg.is_a?( Integer )
228
+ lpad_int( arg )
229
+ elsif arg.size == 20
230
+ lpad( arg )
231
+ elsif arg.size == 40
232
+ lpad_hex( arg )
233
+ elsif arg.size == 42 && arg[0,2] == '0x' ## todo/fix: allow 0X too - why? why not?
234
+ lpad_hex( arg[2..-1] )
235
+ else
236
+ raise EncodingError, "Could not parse address: #{arg}"
237
+ end
238
+ end
239
+
240
+
241
+ ###########
242
+ # encoding helpers / utils
243
+
244
+ def rpad( bin, l=32, symbol=BYTE_ZERO ) ## note: same as builtin String#ljust !!!
245
+ return bin if bin.size >= l
246
+ bin + symbol * (l - bin.size)
247
+ end
248
+
249
+ def lpad( bin, l=32, symbol=BYTE_ZERO ) ## note: same as builtin String#rjust !!!
250
+ return bin if bin.size >= l
251
+ symbol * (l - bin.size) + bin
252
+ end
253
+
254
+ def lpad_int( n, l=32, symbol=BYTE_ZERO )
255
+ raise ArgumentError, "Integer invalid or out of range: #{n}" unless n.is_a?(Integer) && n >= 0 && n <= UINT_MAX
256
+ hex = n.to_s(16)
257
+ hex = "0#{hex}" if hex.size.odd?
258
+ bin = [hex].pack("H*")
259
+
260
+ lpad( bin, l, symbol )
261
+ end
262
+
263
+ def lpad_hex( hex, l=32, symbol=BYTE_ZERO )
264
+ raise TypeError, "Value must be a string" unless hex.is_a?( ::String )
265
+ raise TypeError, 'Non-hexadecimal digit found' unless hex =~ /\A[0-9a-fA-F]*\z/
266
+ bin = [hex].pack("H*")
267
+
268
+ lpad( bin, l, symbol )
269
+ end
270
+
271
+
272
+
273
+ def ceil32(x)
274
+ x % 32 == 0 ? x : (x + 32 - x%32)
275
+ end
276
+
277
+ end # class Encoder
278
+ end # module ABI
279
+
@@ -0,0 +1,158 @@
1
+ module ABI
2
+ class Type
3
+
4
+ class ParseError < StandardError; end
5
+
6
+
7
+ ## nested inside Type - why? why not?
8
+ class Parser
9
+
10
+ TUPLE_TYPE_RX = /^\((.*)\)
11
+ ((\[[0-9]*\])*)
12
+ /x
13
+
14
+ def self.parse( type )
15
+ if type =~ TUPLE_TYPE_RX
16
+ types = _parse_tuple_type( $1 )
17
+ dims = _parse_dims( $2 )
18
+
19
+ parsed_types = types.map{ |t| parse( t ) }
20
+
21
+ return _parse_array_type( Tuple.new( parsed_types ), dims )
22
+ end
23
+
24
+ base, sub, dims = _parse_base_type( type )
25
+
26
+ _validate_base_type( base, sub )
27
+
28
+ subtype = case base
29
+ when 'string' then String.new
30
+ when 'bytes' then sub ? FixedBytes.new( sub ) : Bytes.new
31
+ when 'uint' then Uint.new( sub )
32
+ when 'int' then Int.new( sub )
33
+ when 'address' then Address.new
34
+ when 'bool' then Bool.new
35
+ else
36
+ ## puts " type: >#{type}<"
37
+ raise ParseError, "Unrecognized type base: #{base}"
38
+ end
39
+
40
+ _parse_array_type( subtype, dims )
41
+ end
42
+
43
+ ##
44
+ # Crazy regexp to seperate out base type component (eg. uint), size (eg.
45
+ # 256, 128, nil), array component (eg. [], [45], nil)
46
+ #
47
+ BASE_TYPE_RX = /([a-z]*)
48
+ ([0-9]*)
49
+ ((\[[0-9]*\])*)
50
+ /x
51
+
52
+ def self._parse_base_type( str )
53
+ _, base, subscript, dimension = BASE_TYPE_RX.match( str ).to_a
54
+
55
+ ## note: use [Integer,Integer] array in the future for sub
56
+ ## for fixed (e.g. 128x128 => [128,128]) or such
57
+ ## for now always assume single integer (as string)
58
+ sub = subscript == '' ? nil : subscript.to_i
59
+
60
+ ## e.g. turn "[][1][2]" into [-1,1,2]
61
+ ## or "" into [] -- that is, empty array
62
+ dims = _parse_dims( dimension )
63
+
64
+ [base, sub, dims]
65
+ end
66
+
67
+ def self._parse_dims( str )
68
+ dims = str.scan( /\[[0-9]*\]/ )
69
+
70
+ ## note: return -1 for dynamic array size e.g. []
71
+ ## e.g. "[]"[1...-1] => ""
72
+ ## "[0]"[1...-1] => "0"
73
+ ## "[1]"[1...-1] => "1"
74
+ dims = dims.map do |dim|
75
+ size = dim[1...-1]
76
+ size == '' ? -1 : size.to_i
77
+ end
78
+ dims
79
+ end
80
+
81
+ def self._parse_array_type( subtype, dims )
82
+ ##
83
+ ## todo/check - double check if the order in reverse
84
+ ## in solidity / abi encoding / decoding?
85
+ ##
86
+ dims.each do |dim|
87
+ subtype = if dim == -1
88
+ Array.new( subtype )
89
+ else
90
+ FixedArray.new( subtype, dim )
91
+ end
92
+ end
93
+
94
+ subtype
95
+ end
96
+
97
+
98
+ def self._validate_base_type( base, sub )
99
+ case base
100
+ when 'string'
101
+ # note: string can not have any suffix
102
+ raise ParseError, "String cannot have suffix" if sub
103
+ when 'bytes'
104
+ raise ParseError, "Maximum 32 bytes for fixed-length bytes" if sub && sub > 32
105
+ when 'uint', 'int'
106
+ raise ParseError, "Integer type must have numerical suffix" unless sub
107
+ raise ParseError, "Integer size out of bounds" unless sub >= 8 && sub <= 256
108
+ raise ParseError, "Integer size must be multiple of 8" unless sub % 8 == 0
109
+ when 'address'
110
+ raise ParseError, "Address cannot have suffix" if sub
111
+ when 'bool'
112
+ raise ParseError, "Bool cannot have suffix" if sub
113
+ else
114
+ ## puts " type: >#{type}<"
115
+ raise ParseError, "Unrecognized type base: #{base}"
116
+ end
117
+ end
118
+
119
+
120
+
121
+ def self._parse_tuple_type( str )
122
+ ## note: types assumes string WITHOUT enclosing () e.g.
123
+ ## tuple(string,string,bool) => expected as "string,string,bool"
124
+
125
+ depth = 0
126
+ collected = []
127
+ current = ''
128
+
129
+ ### todo/fix: replace with a simple parser!!!
130
+ ## allow () and move verbose tuple() too!!!
131
+ str.each_char do |c|
132
+ case c
133
+ when ',' then
134
+ if depth == 0
135
+ collected << current
136
+ current = ''
137
+ else
138
+ current += c
139
+ end
140
+ when '(' then
141
+ depth += 1
142
+ current += c
143
+ when ')' then
144
+ depth -= 1
145
+ current += c
146
+ else
147
+ current += c
148
+ end
149
+ end
150
+ collected << current unless current.empty?
151
+
152
+ collected
153
+ end
154
+
155
+ end # class Parser
156
+ end # class Type
157
+ end # module ABI
158
+
@@ -0,0 +1,155 @@
1
+ module ABI
2
+
3
+
4
+ #######
5
+ ## for now use (get inspired)
6
+ ## by the type names used by abi coder in rust
7
+ ## see https://github.com/rust-ethereum/ethabi/blob/master/ethabi/src/param_type/param_type.rs
8
+
9
+
10
+ class Type
11
+
12
+ def self.parse( type ) ## convenience helper
13
+ Parser.parse( type )
14
+ end
15
+
16
+ ##
17
+ # Get the static size of a type, or nil if dynamic.
18
+ #
19
+ # @return [Integer, NilClass] size of static type, or nil for dynamic type
20
+ #
21
+ def size
22
+ ## check/todo: what error to raise for not implemented / method not defined???
23
+ raise ArgumentError, "no required size method defined for Type subclass #{self.class.name}; sorry"
24
+ end
25
+ def dynamic?() size.nil?; end
26
+
27
+ def format
28
+ ## check/todo: what error to raise for not implemented / method not defined???
29
+ raise ArgumentError, "no required format method defined for Type subclass #{self.class.name}; sorry"
30
+ end
31
+
32
+ ####
33
+ ## default implementation
34
+ ## assume equal if class match (e.g. Address == Address)
35
+ ## - use format string for generic compare - why? why not?
36
+ end
37
+
38
+
39
+
40
+ class Address < Type
41
+ def size() 32; end ## note: address is always 20 bytes; BUT uses 32 bytes (with padding)
42
+ def format() 'address'; end
43
+ def ==(another_type) another_type.kind_of?( Address ); end
44
+ end # class Address
45
+
46
+ class Bytes < Type
47
+ def size() nil; end ## note: dynamic (not known at compile-time)
48
+ def format() 'bytes'; end
49
+ def ==(another_type) another_type.kind_of?( Bytes ); end
50
+ end # class Bytes
51
+
52
+ class FixedBytes < Type
53
+ attr_reader :length
54
+ def initialize( length )
55
+ @length = length # in bytes (1,2,...32)
56
+ end
57
+ def size() 32; end ## note: always uses 32 bytes (with padding)
58
+ def format() "bytes#{@length}"; end
59
+ def ==(another_type)
60
+ another_type.kind_of?( FixedBytes ) && @length == another_type.length
61
+ end
62
+ end # class FixedBytes
63
+
64
+ class Int < Type
65
+ attr_reader :bits
66
+ def initialize( bits=256 )
67
+ @bits = bits # in bits (8,16,...256)
68
+ end
69
+ def size() 32; end ## note: always uses 32 bytes (with padding)
70
+ def format() "int#{@bits}"; end
71
+ def ==(another_type)
72
+ another_type.kind_of?( Int ) && @bits == another_type.bits
73
+ end
74
+ end # class Int
75
+
76
+ class Uint < Type
77
+ attr_reader :bits
78
+ def initialize( bits=256 )
79
+ @bits = bits # in bits (8,16,...256)
80
+ end
81
+ def size() 32; end ## note: always uses 32 bytes (with padding)
82
+ def format() "uint#{@bits}"; end
83
+ def ==(another_type)
84
+ another_type.kind_of?( Uint ) && @bits == another_type.bits
85
+ end
86
+ end # class Uint
87
+
88
+ class Bool < Type
89
+ def size() 32; end ## note: always uses 32 bytes (with padding)
90
+ def format() 'bool'; end
91
+ def ==(another_type) another_type.kind_of?( Bool ); end
92
+ end # class Bool
93
+
94
+ class String < Type
95
+ def size() nil; end ## note: dynamic (not known at compile-time)
96
+ def format() 'string'; end
97
+ def ==(another_type) another_type.kind_of?( String ); end
98
+ end # class String
99
+
100
+ class Array < Type
101
+ attr_reader :subtype
102
+ def initialize( subtype )
103
+ @subtype = subtype
104
+ end
105
+ def size() nil; end ## note: dynamic (not known at compile-time)
106
+ def format() "#{@subtype.format}[]"; end
107
+ def ==(another_type)
108
+ another_type.kind_of?( Array ) && @subtype == another_type.subtype
109
+ end
110
+ end # class Array
111
+
112
+ class FixedArray < Type
113
+ attr_reader :subtype
114
+ attr_reader :dim
115
+ def initialize( subtype, dim )
116
+ @subtype = subtype
117
+ @dim = dim
118
+ end
119
+
120
+ def size
121
+ @subtype.dynamic? ? nil : @dim * subtype.size
122
+ end
123
+ def format() "#{@subtype.format}[#{@dim}]"; end
124
+ def ==(another_type)
125
+ another_type.kind_of?( FixedArray ) &&
126
+ @dim == another_type.dim &&
127
+ @subtype == another_type.subtype
128
+ end
129
+ end # class FixedArray
130
+
131
+ class Tuple < Type
132
+ attr_reader :types
133
+
134
+ def initialize( types )
135
+ @types = types
136
+ end
137
+
138
+ def size
139
+ s = 0
140
+ @types.each do |type|
141
+ ts = type.size
142
+ return nil if ts.nil?
143
+ s += ts
144
+ end
145
+ s
146
+ end
147
+ def format
148
+ "(#{@types.map {|t| t.format }.join(',')})" ## rebuild minimal string
149
+ end
150
+ def ==(another_type)
151
+ another_type.kind_of?( Tuple ) && @types == another_type.types
152
+ end
153
+ end # class Tuple
154
+ end # module ABI
155
+
@@ -1,7 +1,7 @@
1
1
 
2
2
  module ABICoder
3
3
  MAJOR = 0
4
- MINOR = 0
4
+ MINOR = 1
5
5
  PATCH = 1
6
6
  VERSION = [MAJOR,MINOR,PATCH].join('.')
7
7
 
data/lib/abicoder.rb CHANGED
@@ -4,13 +4,65 @@
4
4
  require_relative 'abicoder/version' # note: let version always go first
5
5
 
6
6
 
7
-
8
7
  module ABI
8
+ ###################
9
+ ### some (shared) constants (move to constants.rb or such - why? why not?)
10
+
11
+ ## todo/check: use auto-freeze string literals magic comment - why? why not?
12
+ ##
13
+ ## todo/fix: move BYTE_EMPTY, BYTE_ZERO, BYTE_ONE to upstream to bytes gem
14
+ ## and make "global" constants - why? why not?
15
+
16
+ BYTE_EMPTY = "".b.freeze
17
+ BYTE_ZERO = "\x00".b.freeze
18
+ BYTE_ONE = "\x01".b.freeze ## note: used for encoding bool for now
19
+
9
20
 
21
+ UINT_MAX = 2**256 - 1 ## same as 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
22
+ UINT_MIN = 0
23
+ INT_MAX = 2**255 - 1 ## same as 57896044618658097711785492504343953926634992332820282019728792003956564819967
24
+ INT_MIN = -2**255 ## same as -57896044618658097711785492504343953926634992332820282019728792003956564819968
10
25
 
11
26
  end # module ABI
12
27
 
13
28
 
29
+ require_relative 'abicoder/types'
30
+ require_relative 'abicoder/parser'
31
+
32
+
33
+ require_relative 'abicoder/encoder'
34
+ require_relative 'abicoder/decoder'
35
+
36
+
37
+ module ABI
38
+ def self.encoder
39
+ @encoder ||= Encoder.new
40
+ end
41
+ def self.decoder
42
+ @decoder ||= Decoder.new
43
+ end
44
+
45
+
46
+ def self.encode( types, args )
47
+ encoder.encode( types, args )
48
+ end
49
+
50
+ def self.decode( types, data, raise_errors = false )
51
+ decoder.decode( types, data, raise_errors )
52
+ end
53
+
54
+ ## add alternate _abi names - why? why not?
55
+ class << self
56
+ alias_method :encode_abi, :encode
57
+ alias_method :decode_abi, :decode
58
+ end
59
+ end ## module ABI
60
+
61
+
62
+ ################
63
+ ## add convenience alternate spellings - why? why not?
64
+ Abi = ABI
65
+
14
66
 
15
67
 
16
68
  puts ABICoder.banner ## say hello
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: abicoder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gerald Bauer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-30 00:00:00.000000000 Z
11
+ date: 2023-01-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rdoc
@@ -44,8 +44,9 @@ dependencies:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
46
  version: '3.23'
47
- description: abicoder - application binary interface (abi) encoding / decoding machinery
48
- / helper for Ethereum & Co. (blockchain) contracts
47
+ description: abicoder - 'lite' application binary interface (abi) encoding / decoding
48
+ machinery / helper for Ethereum & Co. (blockchain) contracts with zero-dependencies
49
+ for easy (re)use
49
50
  email: wwwmake@googlegroups.com
50
51
  executables: []
51
52
  extensions: []
@@ -59,6 +60,10 @@ files:
59
60
  - README.md
60
61
  - Rakefile
61
62
  - lib/abicoder.rb
63
+ - lib/abicoder/decoder.rb
64
+ - lib/abicoder/encoder.rb
65
+ - lib/abicoder/parser.rb
66
+ - lib/abicoder/types.rb
62
67
  - lib/abicoder/version.rb
63
68
  homepage: https://github.com/rubycocos/blockchain
64
69
  licenses:
@@ -84,6 +89,7 @@ requirements: []
84
89
  rubygems_version: 3.3.7
85
90
  signing_key:
86
91
  specification_version: 4
87
- summary: abicoder - application binary interface (abi) encoding / decoding machinery
88
- / helper for Ethereum & Co. (blockchain) contracts
92
+ summary: abicoder - 'lite' application binary interface (abi) encoding / decoding
93
+ machinery / helper for Ethereum & Co. (blockchain) contracts with zero-dependencies
94
+ for easy (re)use
89
95
  test_files: []