abicoder 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []