abicoder 0.1.1 → 1.0.0
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 +4 -4
- data/README.md +1 -1
- data/Rakefile +1 -1
- data/lib/abicoder/decoder.rb +54 -55
- data/lib/abicoder/encoder.rb +33 -34
- data/lib/abicoder/version.rb +3 -3
- data/lib/abicoder.rb +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6aed5c766df24d4cf394f10fdaec00a7a42375cf85c5eab0a07d8f7f22ad3ec
|
4
|
+
data.tar.gz: 80c9a39735f1e383f168fdca5980d66acbf039a2bfbffe15866f0fb89a7090cf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9142a552978835f45a2be5bd80190905b9feeb519f135c8ae72f428826a2e7ccec467a1dbbfbfef8ddd2b097b1397f008429379230abc8ac6bcaf4aa53f7ec99
|
7
|
+
data.tar.gz: 37a0e2773d33a8c74881ccd8ab137e362dacde34c6dd076be0eed6ffe5b22f302ac8cdd2220e94ed124b5036a836b21b7977c0629daf9e749c45b2465e4aab47
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Application Binary Inteface (ABI) Coder For Ethereum & Co.
|
2
2
|
|
3
|
-
abicoder - "lite" application binary interface (abi) encoding / decoding machinery / helper for Ethereum & Co. (blockchain) contracts with zero-dependencies for easy (re)use
|
3
|
+
abicoder - "lite" application binary interface (abi) encoding / decoding machinery / helper (incl. nested arrays and/or tuples) 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)
|
data/Rakefile
CHANGED
@@ -13,7 +13,7 @@ Hoe.spec 'abicoder' do
|
|
13
13
|
|
14
14
|
self.version = ABICoder::VERSION
|
15
15
|
|
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"
|
16
|
+
self.summary = "abicoder - 'lite' application binary interface (abi) encoding / decoding machinery / helper (incl. nested arrays and/or tuples) for Ethereum & Co. (blockchain) contracts with zero-dependencies for easy (re)use"
|
17
17
|
self.description = summary
|
18
18
|
|
19
19
|
self.urls = { home: 'https://github.com/rubycocos/blockchain' }
|
data/lib/abicoder/decoder.rb
CHANGED
@@ -7,6 +7,10 @@ class Decoder
|
|
7
7
|
# Decodes multiple arguments using the head/tail mechanism.
|
8
8
|
#
|
9
9
|
def decode( types, data, raise_errors = false )
|
10
|
+
##
|
11
|
+
## todo/check: always change data (string) to binary encoding (e.g. data = data.b )
|
12
|
+
## or such - why? why not?
|
13
|
+
|
10
14
|
## for convenience check if types is a String
|
11
15
|
## otherwise assume ABI::Type already
|
12
16
|
types = types.map { |type| type.is_a?( Type ) ? type : Type.parse( type ) }
|
@@ -14,7 +18,9 @@ class Decoder
|
|
14
18
|
outputs = [nil] * types.size
|
15
19
|
start_positions = [nil] * types.size + [data.size]
|
16
20
|
|
17
|
-
# TODO: refactor, a reverse iteration will be better
|
21
|
+
# TODO: refactor, a reverse iteration will be better - why? why not?
|
22
|
+
# try to simplify / clean-up code - possible? why? why not?
|
23
|
+
|
18
24
|
pos = 0
|
19
25
|
types.each_with_index do |t, i|
|
20
26
|
# If a type is static, grab the data directly, otherwise record its
|
@@ -29,7 +35,7 @@ class Decoder
|
|
29
35
|
end
|
30
36
|
end
|
31
37
|
|
32
|
-
start_positions[i] =
|
38
|
+
start_positions[i] = decode_uint256(data[pos, 32])
|
33
39
|
|
34
40
|
if start_positions[i]>data.size-1
|
35
41
|
if raise_errors
|
@@ -50,7 +56,19 @@ class Decoder
|
|
50
56
|
else
|
51
57
|
## puts "step 1 - decode item [#{i}] - #{t.format} size: #{t.size} dynamic? #{t.dynamic?}"
|
52
58
|
|
53
|
-
|
59
|
+
count = t.size
|
60
|
+
## was zero_padding( data, pos, t.size, start_positions )
|
61
|
+
## inline for now and clean-up later - why? why not?
|
62
|
+
outputs[i] = if pos >= data.size
|
63
|
+
start_positions[start_positions.size-1] += count
|
64
|
+
BYTE_ZERO*count
|
65
|
+
elsif pos + count > data.size
|
66
|
+
start_positions[start_positions.size-1] += ( count - (data.size-pos))
|
67
|
+
data[pos,data.size-pos] + BYTE_ZERO*( count - (data.size-pos))
|
68
|
+
else
|
69
|
+
data[pos, count]
|
70
|
+
end
|
71
|
+
|
54
72
|
pos += t.size
|
55
73
|
end
|
56
74
|
end
|
@@ -83,7 +101,7 @@ class Decoder
|
|
83
101
|
end
|
84
102
|
end
|
85
103
|
|
86
|
-
if outputs.include?(nil)
|
104
|
+
if outputs.include?( nil )
|
87
105
|
if raise_errors
|
88
106
|
raise DecodingError, "Not all data can be parsed"
|
89
107
|
else
|
@@ -101,106 +119,87 @@ class Decoder
|
|
101
119
|
|
102
120
|
|
103
121
|
|
104
|
-
def decode_type( type, arg )
|
105
|
-
return nil if arg.nil? || arg.empty?
|
106
122
|
|
123
|
+
def decode_type( type, data )
|
124
|
+
return nil if data.nil? || data.empty?
|
107
125
|
|
108
|
-
if type.is_a?(
|
109
|
-
|
110
|
-
data = arg[32..-1]
|
111
|
-
data[0, l]
|
112
|
-
elsif type.is_a?( Tuple )
|
113
|
-
arg ? decode(type.types, arg) : []
|
126
|
+
if type.is_a?( Tuple ) ## todo: support empty (unit) tuple - why? why not?
|
127
|
+
decode( type.types, data )
|
114
128
|
elsif type.is_a?( FixedArray ) # static-sized arrays
|
115
129
|
l = type.dim
|
116
130
|
subtype = type.subtype
|
117
131
|
if subtype.dynamic?
|
118
|
-
start_positions = (0...l).map {|i|
|
119
|
-
start_positions.push
|
132
|
+
start_positions = (0...l).map {|i| decode_uint256(data[32*i, 32]) }
|
133
|
+
start_positions.push( data.size )
|
120
134
|
|
121
|
-
outputs = (0...l).map {|i|
|
135
|
+
outputs = (0...l).map {|i| data[start_positions[i]...start_positions[i+1]] }
|
122
136
|
|
123
137
|
outputs.map {|out| decode_type(subtype, out) }
|
124
138
|
else
|
125
|
-
(0...l).map {|i| decode_type(subtype,
|
139
|
+
(0...l).map {|i| decode_type(subtype, data[subtype.size*i, subtype.size]) }
|
126
140
|
end
|
127
141
|
elsif type.is_a?( Array )
|
128
|
-
l =
|
142
|
+
l = decode_uint256( data[0,32] )
|
129
143
|
raise DecodingError, "Too long length: #{l}" if l > 100000
|
130
144
|
subtype = type.subtype
|
131
145
|
|
132
146
|
if subtype.dynamic?
|
133
|
-
raise DecodingError, "Not enough data for head" unless
|
147
|
+
raise DecodingError, "Not enough data for head" unless data.size >= 32 + 32*l
|
134
148
|
|
135
|
-
start_positions = (1..l).map {|i| 32+
|
136
|
-
start_positions.push
|
149
|
+
start_positions = (1..l).map {|i| 32+decode_uint256(data[32*i, 32]) }
|
150
|
+
start_positions.push( data.size )
|
137
151
|
|
138
|
-
outputs = (0...l).map {|i|
|
152
|
+
outputs = (0...l).map {|i| data[start_positions[i]...start_positions[i+1]] }
|
139
153
|
|
140
154
|
outputs.map {|out| decode_type(subtype, out) }
|
141
155
|
else
|
142
|
-
(0...l).map {|i| decode_type(subtype,
|
156
|
+
(0...l).map {|i| decode_type(subtype, data[32 + subtype.size*i, subtype.size]) }
|
143
157
|
end
|
144
158
|
else
|
145
|
-
decode_primitive_type( type,
|
159
|
+
decode_primitive_type( type, data )
|
146
160
|
end
|
147
161
|
end
|
148
162
|
|
149
163
|
|
150
164
|
def decode_primitive_type(type, data)
|
151
165
|
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
166
|
when Uint
|
164
|
-
|
167
|
+
decode_uint256( data )
|
165
168
|
when Int
|
166
|
-
u =
|
169
|
+
u = decode_uint256( data )
|
167
170
|
u >= 2**(type.bits-1) ? (u - 2**type.bits) : u
|
168
171
|
when Bool
|
169
172
|
data[-1] == BYTE_ONE
|
173
|
+
when String
|
174
|
+
## note: convert to a string (with UTF_8 encoding NOT BINARY!!!)
|
175
|
+
size = decode_uint256( data[0,32] )
|
176
|
+
data[32..-1][0,size].force_encoding( Encoding::UTF_8 )
|
177
|
+
when Bytes
|
178
|
+
size = decode_uint256( data[0,32] )
|
179
|
+
data[32..-1][0,size]
|
180
|
+
when FixedBytes
|
181
|
+
data[0, type.length]
|
182
|
+
when Address
|
183
|
+
## note: convert to a hex string (with UTF_8 encoding NOT BINARY!!!)
|
184
|
+
data[12..-1].unpack("H*").first.force_encoding( Encoding::UTF_8 )
|
170
185
|
else
|
171
186
|
raise DecodingError, "Unknown primitive type: #{type.class.name} #{type.format}"
|
172
187
|
end
|
173
188
|
end
|
174
189
|
|
175
190
|
|
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
191
|
|
189
192
|
###########
|
190
193
|
# decoding helpers / utils
|
191
194
|
|
192
|
-
def
|
193
|
-
bin = bin.sub( /\A(\x00)+/, '' ) ## keep "performance" shortcut - why? why not?
|
195
|
+
def decode_uint256( bin )
|
196
|
+
# bin = bin.sub( /\A(\x00)+/, '' ) ## keep "performance" shortcut - why? why not?
|
194
197
|
### todo/check - allow nil - why? why not?
|
195
198
|
## raise DeserializationError, "Invalid serialization (not minimal length)" if !@size && serial.size > 0 && serial[0] == BYTE_ZERO
|
196
|
-
bin = bin || BYTE_ZERO
|
199
|
+
# bin = bin || BYTE_ZERO
|
197
200
|
bin.unpack("H*").first.to_i(16)
|
198
201
|
end
|
199
202
|
|
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
203
|
|
205
204
|
|
206
205
|
end # class Decoder
|
data/lib/abicoder/encoder.rb
CHANGED
@@ -18,11 +18,11 @@ class Encoder
|
|
18
18
|
|
19
19
|
##
|
20
20
|
# Encodes multiple arguments using the head/tail mechanism.
|
21
|
+
# returns binary string (with BINARY / ASCII_8BIT encoding)
|
21
22
|
#
|
22
23
|
def encode( types, args )
|
23
|
-
|
24
|
-
|
25
|
-
## raise ArgumentError - expected x arguments got y!!!
|
24
|
+
## enforce args.size and types.size must match - why? why not?
|
25
|
+
raise ArgumentError, "Wrong number of args: found #{args.size}, expecting #{types.size}" unless args.size == types.size
|
26
26
|
|
27
27
|
|
28
28
|
## for convenience check if types is a String
|
@@ -35,7 +35,7 @@ class Encoder
|
|
35
35
|
.map {|type| type.size || 32 }
|
36
36
|
.sum
|
37
37
|
|
38
|
-
head, tail = '', ''
|
38
|
+
head, tail = ''.b, ''.b ## note: use string buffer with BINARY / ASCII_8BIT encoding!!!
|
39
39
|
types.each_with_index do |type, i|
|
40
40
|
if type.dynamic?
|
41
41
|
head += encode_uint256( head_size + tail.size )
|
@@ -57,10 +57,6 @@ class Encoder
|
|
57
57
|
# @return [String] encoded bytes
|
58
58
|
#
|
59
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
60
|
if type.is_a?( String )
|
65
61
|
encode_string( arg )
|
66
62
|
elsif type.is_a?( Bytes )
|
@@ -82,7 +78,7 @@ class Encoder
|
|
82
78
|
def encode_dynamic_array( type, args )
|
83
79
|
raise ArgumentError, "arg must be an array" unless args.is_a?(::Array)
|
84
80
|
|
85
|
-
head, tail = '', ''
|
81
|
+
head, tail = ''.b, ''.b ## note: use string buffer with BINARY / ASCII_8BIT encoding!!!
|
86
82
|
|
87
83
|
if type.is_a?( Array ) ## dynamic array
|
88
84
|
head += encode_uint256( args.size )
|
@@ -123,7 +119,7 @@ class Encoder
|
|
123
119
|
.map {|type| type.size || 32 }
|
124
120
|
.sum
|
125
121
|
|
126
|
-
head, tail = '', ''
|
122
|
+
head, tail = ''.b, ''.b ## note: use string buffer with BINARY / ASCII_8BIT encoding!!!
|
127
123
|
tuple.types.each_with_index do |type, i|
|
128
124
|
if type.dynamic?
|
129
125
|
head += encode_uint256( head_size + tail.size )
|
@@ -164,19 +160,22 @@ class Encoder
|
|
164
160
|
|
165
161
|
|
166
162
|
def encode_bool( arg )
|
163
|
+
## raise EncodingError or ArgumentError - why? why not?
|
167
164
|
raise ArgumentError, "arg is not bool: #{arg}" unless arg.is_a?(TrueClass) || arg.is_a?(FalseClass)
|
168
|
-
lpad_int( arg ? 1 : 0 )
|
165
|
+
lpad( arg ? BYTE_ONE : BYTE_ZERO ) ## was lpad_int( arg ? 1 : 0 )
|
169
166
|
end
|
170
167
|
|
171
168
|
|
172
169
|
def encode_uint256( arg ) encode_uint( arg, 256 ); end
|
173
170
|
def encode_uint( arg, bits )
|
171
|
+
## raise EncodingError or ArgumentError - why? why not?
|
174
172
|
raise ArgumentError, "arg is not integer: #{arg}" unless arg.is_a?(Integer)
|
175
173
|
raise ValueOutOfBounds, arg unless arg >= 0 && arg < 2**bits
|
176
174
|
lpad_int( arg )
|
177
175
|
end
|
178
176
|
|
179
177
|
def encode_int( arg, bits )
|
178
|
+
## raise EncodingError or ArgumentError - why? why not?
|
180
179
|
raise ArgumentError, "arg is not integer: #{arg}" unless arg.is_a?(Integer)
|
181
180
|
raise ValueOutOfBounds, arg unless arg >= -2**(bits-1) && arg < 2**(bits-1)
|
182
181
|
lpad_int( arg % 2**bits )
|
@@ -184,31 +183,21 @@ class Encoder
|
|
184
183
|
|
185
184
|
|
186
185
|
def encode_string( arg )
|
187
|
-
|
188
|
-
|
189
|
-
|
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
|
186
|
+
## raise EncodingError or ArgumentError - why? why not?
|
187
|
+
raise EncodingError, "Expecting string: #{arg}" unless arg.is_a?(::String)
|
188
|
+
arg = arg.b if arg.encoding != Encoding::BINARY ## was: name == 'UTF-8'
|
200
189
|
|
201
190
|
raise ValueOutOfBounds, "Integer invalid or out of range: #{arg.size}" if arg.size > UINT_MAX
|
202
191
|
size = lpad_int( arg.size )
|
203
192
|
value = rpad( arg, ceil32(arg.size) )
|
204
|
-
|
205
193
|
size + value
|
206
194
|
end
|
207
195
|
|
208
196
|
|
209
197
|
def encode_bytes( arg, length=nil )
|
198
|
+
## raise EncodingError or ArgumentError - why? why not?
|
210
199
|
raise EncodingError, "Expecting string: #{arg}" unless arg.is_a?(::String)
|
211
|
-
arg = arg.b
|
200
|
+
arg = arg.b if arg.encoding != Encoding::BINARY
|
212
201
|
|
213
202
|
if length # fixed length type
|
214
203
|
raise ValueOutOfBounds, "invalid bytes length #{length}" if arg.size > length
|
@@ -227,6 +216,8 @@ class Encoder
|
|
227
216
|
if arg.is_a?( Integer )
|
228
217
|
lpad_int( arg )
|
229
218
|
elsif arg.size == 20
|
219
|
+
## note: make sure encoding is always binary!!!
|
220
|
+
arg = arg.b if arg.encoding != Encoding::BINARY
|
230
221
|
lpad( arg )
|
231
222
|
elsif arg.size == 40
|
232
223
|
lpad_hex( arg )
|
@@ -238,34 +229,42 @@ class Encoder
|
|
238
229
|
end
|
239
230
|
|
240
231
|
|
232
|
+
|
241
233
|
###########
|
242
234
|
# encoding helpers / utils
|
235
|
+
# with "hard-coded" fill symbol as BYTE_ZERO
|
243
236
|
|
244
|
-
def rpad( bin, l=32
|
237
|
+
def rpad( bin, l=32 ) ## note: same as builtin String#ljust !!!
|
238
|
+
# note: default l word is 32 bytes
|
245
239
|
return bin if bin.size >= l
|
246
|
-
bin +
|
240
|
+
bin + BYTE_ZERO * (l - bin.size)
|
247
241
|
end
|
248
242
|
|
249
|
-
|
243
|
+
|
244
|
+
## rename to lpad32 or such - why? why not?
|
245
|
+
def lpad( bin ) ## note: same as builtin String#rjust !!!
|
246
|
+
l=32 # note: default l word is 32 bytes
|
250
247
|
return bin if bin.size >= l
|
251
|
-
|
248
|
+
BYTE_ZERO * (l - bin.size) + bin
|
252
249
|
end
|
253
250
|
|
254
|
-
|
251
|
+
## rename to lpad32_int or such - why? why not?
|
252
|
+
def lpad_int( n )
|
255
253
|
raise ArgumentError, "Integer invalid or out of range: #{n}" unless n.is_a?(Integer) && n >= 0 && n <= UINT_MAX
|
256
254
|
hex = n.to_s(16)
|
257
255
|
hex = "0#{hex}" if hex.size.odd?
|
258
256
|
bin = [hex].pack("H*")
|
259
257
|
|
260
|
-
lpad( bin
|
258
|
+
lpad( bin )
|
261
259
|
end
|
262
260
|
|
263
|
-
|
261
|
+
## rename to lpad32_hex or such - why? why not?
|
262
|
+
def lpad_hex( hex )
|
264
263
|
raise TypeError, "Value must be a string" unless hex.is_a?( ::String )
|
265
264
|
raise TypeError, 'Non-hexadecimal digit found' unless hex =~ /\A[0-9a-fA-F]*\z/
|
266
265
|
bin = [hex].pack("H*")
|
267
266
|
|
268
|
-
lpad( bin
|
267
|
+
lpad( bin )
|
269
268
|
end
|
270
269
|
|
271
270
|
|
data/lib/abicoder/version.rb
CHANGED
data/lib/abicoder.rb
CHANGED
@@ -13,7 +13,7 @@ module ABI
|
|
13
13
|
## todo/fix: move BYTE_EMPTY, BYTE_ZERO, BYTE_ONE to upstream to bytes gem
|
14
14
|
## and make "global" constants - why? why not?
|
15
15
|
|
16
|
-
BYTE_EMPTY = "".b.freeze
|
16
|
+
## BYTE_EMPTY = "".b.freeze
|
17
17
|
BYTE_ZERO = "\x00".b.freeze
|
18
18
|
BYTE_ONE = "\x01".b.freeze ## note: used for encoding bool for now
|
19
19
|
|
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.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gerald Bauer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-01-
|
11
|
+
date: 2023-01-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rdoc
|
@@ -45,8 +45,8 @@ dependencies:
|
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: '3.23'
|
47
47
|
description: abicoder - 'lite' application binary interface (abi) encoding / decoding
|
48
|
-
machinery / helper for Ethereum & Co. (blockchain)
|
49
|
-
for easy (re)use
|
48
|
+
machinery / helper (incl. nested arrays and/or tuples) for Ethereum & Co. (blockchain)
|
49
|
+
contracts with zero-dependencies for easy (re)use
|
50
50
|
email: wwwmake@googlegroups.com
|
51
51
|
executables: []
|
52
52
|
extensions: []
|
@@ -90,6 +90,6 @@ rubygems_version: 3.3.7
|
|
90
90
|
signing_key:
|
91
91
|
specification_version: 4
|
92
92
|
summary: abicoder - 'lite' application binary interface (abi) encoding / decoding
|
93
|
-
machinery / helper for Ethereum & Co. (blockchain)
|
94
|
-
for easy (re)use
|
93
|
+
machinery / helper (incl. nested arrays and/or tuples) for Ethereum & Co. (blockchain)
|
94
|
+
contracts with zero-dependencies for easy (re)use
|
95
95
|
test_files: []
|