Module: Klay::Abi

Extended by:
Abi
Included in:
Abi
Defined in:
lib/klay/abi.rb,
lib/klay/abi/type.rb

Overview

Provides a Ruby implementation of the Klaytn Applicatoin Binary Interface (ABI).

Defined Under Namespace

Classes: DecodingError, EncodingError, Type, ValueOutOfBounds

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.decode(types, data) ⇒ Array

Decodes Application Binary Interface (ABI) data. It accepts multiple arguments and decodes using the head/tail mechanism.

Parameters:

  • types (Array)

    the ABI to be decoded.

  • data (String)

    ABI data to be decoded.

Returns:

  • (Array)

    the decoded ABI data.

Raises:



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/klay/abi.rb', line 148

def decode(types, data)

  # accept hex abi but decode it first
  data = Util.hex_to_bin data if Util.is_hex? data

  # parse all types
  parsed_types = types.map { |t| Type.parse(t) }

  # prepare output data
  outputs = [nil] * types.size
  start_positions = [nil] * types.size + [data.size]
  pos = 0
  parsed_types.each_with_index do |t, i|
    if t.is_dynamic?

      # record start position for dynamic type
      start_positions[i] = Util.deserialize_big_endian_to_int(data[pos, 32])
      j = i - 1
      while j >= 0 and start_positions[j].nil?
        start_positions[j] = start_positions[i]
        j -= 1
      end
      pos += 32
    else

      # get data directly for static types
      outputs[i] = data[pos, t.size]
      pos += t.size
    end
  end

  # add start position equal the length of the entire data
  j = types.size - 1
  while j >= 0 and start_positions[j].nil?
    start_positions[j] = start_positions[types.size]
    j -= 1
  end
  raise DecodingError, "Not enough data for head" unless pos <= data.size

  # add dynamic types
  parsed_types.each_with_index do |t, i|
    if t.is_dynamic?
      offset, next_offset = start_positions[i, 2]
      outputs[i] = data[offset...next_offset]
    end
  end

  # return the decoded ABI types and data
  return parsed_types.zip(outputs).map { |(type, out)| decode_type(type, out) }
end

.decode_primitive_type(type, data) ⇒ String

Decodes primitive types.

Parameters:

  • type (Klay::Abi::Type)

    type to be decoded.

  • data (String)

    encoded primitive type data string.

Returns:

  • (String)

    the decoded data for the type.

Raises:



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/klay/abi.rb', line 241

def decode_primitive_type(type, data)
  case type.base_type
  when "address"

    # decoded address with 0x-prefix
    return "0x#{Util.bin_to_hex data[12..-1]}"
  when "string", "bytes"
    if type.sub_type.empty?
      size = Util.deserialize_big_endian_to_int data[0, 32]

      # decoded dynamic-sized array
      return data[32..-1][0, size]
    else

      # decoded static-sized array
      return data[0, type.sub_type.to_i]
    end
  when "hash"

    # decoded hash
    return data[(32 - type.sub_type.to_i), type.sub_type.to_i]
  when "uint"

    # decoded unsigned integer
    return Util.deserialize_big_endian_to_int data
  when "int"
    u = Util.deserialize_big_endian_to_int data
    i = u >= 2 ** (type.sub_type.to_i - 1) ? (u - 2 ** type.sub_type.to_i) : u

    # decoded integer
    return i
  when "ureal", "ufixed"
    high, low = type.sub_type.split("x").map(&:to_i)

    # decoded unsigned fixed point numeric
    return Util.deserialize_big_endian_to_int(data) * 1.0 / 2 ** low
  when "real", "fixed"
    high, low = type.sub_type.split("x").map(&:to_i)
    u = Util.deserialize_big_endian_to_int data
    i = u >= 2 ** (high + low - 1) ? (u - 2 ** (high + low)) : u

    # decoded fixed point numeric
    return i * 1.0 / 2 ** low
  when "bool"

    # decoded boolean
    return data[-1] == Constant::BYTE_ONE
  else
    raise DecodingError, "Unknown primitive type: #{type.base_type}"
  end
end

.decode_type(type, arg) ⇒ String

Decodes a specific value, either static or dynamic.

Parameters:

  • type (Klay::Abi::Type)

    type to be decoded.

  • arg (String)

    encoded type data string.

Returns:

  • (String)

    the decoded data for the type.

Raises:



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/klay/abi.rb', line 205

def decode_type(type, arg)
  if %w(string bytes).include?(type.base_type) and type.sub_type.empty?
    l = Util.deserialize_big_endian_to_int arg[0, 32]
    data = arg[32..-1]
    raise DecodingError, "Wrong data size for string/bytes object" unless data.size == Util.ceil32(l)

    # decoded strings and bytes
    return data[0, l]
  elsif type.is_dynamic?
    l = Util.deserialize_big_endian_to_int arg[0, 32]
    nested_sub = type.nested_sub

    # ref https://github.com/ethereum/tests/issues/691
    raise NotImplementedError, "Decoding dynamic arrays with nested dynamic sub-types is not implemented for ABI." if nested_sub.is_dynamic?

    # decoded dynamic-sized arrays
    return (0...l).map { |i| decode_type(nested_sub, arg[32 + nested_sub.size * i, nested_sub.size]) }
  elsif !type.dimensions.empty?
    l = type.dimensions.last[0]
    nested_sub = type.nested_sub

    # decoded static-size arrays
    return (0...l).map { |i| decode_type(nested_sub, arg[nested_sub.size * i, nested_sub.size]) }
  else

    # decoded primitive types
    return decode_primitive_type type, arg
  end
end

.encode(types, args) ⇒ String

Encodes Application Binary Interface (ABI) data. It accepts multiple arguments and encodes using the head/tail mechanism.

Parameters:

  • types (Array)

    types to be ABI-encoded.

  • args (Array)

    values to be ABI-encoded.

Returns:

  • (String)

    the encoded ABI data.



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/klay/abi.rb', line 44

def encode(types, args)

  # prase all types
  parsed_types = types.map { |t| Type.parse(t) }

  # prepare the "head"
  head_size = (0...args.size)
    .map { |i| parsed_types[i].size or 32 }
    .reduce(0, &:+)
  head, tail = "", ""

  # encode types and arguments
  args.each_with_index do |arg, i|
    if parsed_types[i].is_dynamic?
      head += encode_type Type.size_type, head_size + tail.size
      tail += encode_type parsed_types[i], arg
    else
      head += encode_type parsed_types[i], arg
    end
  end

  # return the encoded ABI blob
  return "#{head}#{tail}"
end

.encode_primitive_type(type, arg) ⇒ String

Encodes primitive types.

Parameters:

  • type (Klay::Abi::Type)

    type to be encoded.

  • arg (String|Number)

    value to be encoded.

Returns:

  • (String)

    the encoded primitive type.

Raises:



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/klay/abi.rb', line 119

def encode_primitive_type(type, arg)
  case type.base_type
  when "uint"
    return encode_uint arg, type
  when "bool"
    return encode_bool arg
  when "int"
    return encode_int arg, type
  when "ureal", "ufixed"
    return encode_ufixed arg, type
  when "real", "fixed"
    return encode_fixed arg, type
  when "string", "bytes"
    return encode_bytes arg, type
  when "hash"
    return encode_hash arg, type
  when "address"
    return encode_address arg
  else
    raise EncodingError, "Unhandled type: #{type.base_type} #{type.sub_type}"
  end
end

.encode_type(type, arg) ⇒ String

Encodes a specific value, either static or dynamic.

Parameters:

  • type (Klay::Abi::Type)

    type to be encoded.

  • arg (String|Number)

    value to be encoded.

Returns:

  • (String)

    the encoded type.

Raises:



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/klay/abi.rb', line 75

def encode_type(type, arg)
  if %w(string bytes).include? type.base_type and type.sub_type.empty?
    raise EncodingError, "Argument must be a String" unless arg.instance_of? String

    # encodes strings and bytes
    size = encode_type Type.size_type, arg.size
    padding = Constant::BYTE_ZERO * (Util.ceil32(arg.size) - arg.size)
    return "#{size}#{arg}#{padding}"
  elsif type.is_dynamic?
    raise EncodingError, "Argument must be an Array" unless arg.instance_of? Array

    # encodes dynamic-sized arrays
    head, tail = "", ""
    head += encode_type Type.size_type, arg.size
    nested_sub = type.nested_sub
    nested_sub_size = type.nested_sub.size
    arg.size.times do |i|

      # ref https://github.com/ethereum/tests/issues/691
      raise NotImplementedError, "Encoding dynamic arrays with nested dynamic sub-types is not implemented for ABI." if nested_sub.is_dynamic?
      head += encode_type nested_sub, arg[i]
    end
    return "#{head}#{tail}"
  else
    if type.dimensions.empty?

      # encode a primitive type
      return encode_primitive_type type, arg
    else

      # encode static-size arrays
      return arg.map { |x| encode_type(type.nested_sub, x) }.join
    end
  end
end

Instance Method Details

#decode(types, data) ⇒ Array

Decodes Application Binary Interface (ABI) data. It accepts multiple arguments and decodes using the head/tail mechanism.

Parameters:

  • types (Array)

    the ABI to be decoded.

  • data (String)

    ABI data to be decoded.

Returns:

  • (Array)

    the decoded ABI data.

Raises:



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/klay/abi.rb', line 148

def decode(types, data)

  # accept hex abi but decode it first
  data = Util.hex_to_bin data if Util.is_hex? data

  # parse all types
  parsed_types = types.map { |t| Type.parse(t) }

  # prepare output data
  outputs = [nil] * types.size
  start_positions = [nil] * types.size + [data.size]
  pos = 0
  parsed_types.each_with_index do |t, i|
    if t.is_dynamic?

      # record start position for dynamic type
      start_positions[i] = Util.deserialize_big_endian_to_int(data[pos, 32])
      j = i - 1
      while j >= 0 and start_positions[j].nil?
        start_positions[j] = start_positions[i]
        j -= 1
      end
      pos += 32
    else

      # get data directly for static types
      outputs[i] = data[pos, t.size]
      pos += t.size
    end
  end

  # add start position equal the length of the entire data
  j = types.size - 1
  while j >= 0 and start_positions[j].nil?
    start_positions[j] = start_positions[types.size]
    j -= 1
  end
  raise DecodingError, "Not enough data for head" unless pos <= data.size

  # add dynamic types
  parsed_types.each_with_index do |t, i|
    if t.is_dynamic?
      offset, next_offset = start_positions[i, 2]
      outputs[i] = data[offset...next_offset]
    end
  end

  # return the decoded ABI types and data
  return parsed_types.zip(outputs).map { |(type, out)| decode_type(type, out) }
end

#decode_primitive_type(type, data) ⇒ String

Decodes primitive types.

Parameters:

  • type (Klay::Abi::Type)

    type to be decoded.

  • data (String)

    encoded primitive type data string.

Returns:

  • (String)

    the decoded data for the type.

Raises:



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/klay/abi.rb', line 241

def decode_primitive_type(type, data)
  case type.base_type
  when "address"

    # decoded address with 0x-prefix
    return "0x#{Util.bin_to_hex data[12..-1]}"
  when "string", "bytes"
    if type.sub_type.empty?
      size = Util.deserialize_big_endian_to_int data[0, 32]

      # decoded dynamic-sized array
      return data[32..-1][0, size]
    else

      # decoded static-sized array
      return data[0, type.sub_type.to_i]
    end
  when "hash"

    # decoded hash
    return data[(32 - type.sub_type.to_i), type.sub_type.to_i]
  when "uint"

    # decoded unsigned integer
    return Util.deserialize_big_endian_to_int data
  when "int"
    u = Util.deserialize_big_endian_to_int data
    i = u >= 2 ** (type.sub_type.to_i - 1) ? (u - 2 ** type.sub_type.to_i) : u

    # decoded integer
    return i
  when "ureal", "ufixed"
    high, low = type.sub_type.split("x").map(&:to_i)

    # decoded unsigned fixed point numeric
    return Util.deserialize_big_endian_to_int(data) * 1.0 / 2 ** low
  when "real", "fixed"
    high, low = type.sub_type.split("x").map(&:to_i)
    u = Util.deserialize_big_endian_to_int data
    i = u >= 2 ** (high + low - 1) ? (u - 2 ** (high + low)) : u

    # decoded fixed point numeric
    return i * 1.0 / 2 ** low
  when "bool"

    # decoded boolean
    return data[-1] == Constant::BYTE_ONE
  else
    raise DecodingError, "Unknown primitive type: #{type.base_type}"
  end
end

#decode_type(type, arg) ⇒ String

Decodes a specific value, either static or dynamic.

Parameters:

  • type (Klay::Abi::Type)

    type to be decoded.

  • arg (String)

    encoded type data string.

Returns:

  • (String)

    the decoded data for the type.

Raises:



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/klay/abi.rb', line 205

def decode_type(type, arg)
  if %w(string bytes).include?(type.base_type) and type.sub_type.empty?
    l = Util.deserialize_big_endian_to_int arg[0, 32]
    data = arg[32..-1]
    raise DecodingError, "Wrong data size for string/bytes object" unless data.size == Util.ceil32(l)

    # decoded strings and bytes
    return data[0, l]
  elsif type.is_dynamic?
    l = Util.deserialize_big_endian_to_int arg[0, 32]
    nested_sub = type.nested_sub

    # ref https://github.com/ethereum/tests/issues/691
    raise NotImplementedError, "Decoding dynamic arrays with nested dynamic sub-types is not implemented for ABI." if nested_sub.is_dynamic?

    # decoded dynamic-sized arrays
    return (0...l).map { |i| decode_type(nested_sub, arg[32 + nested_sub.size * i, nested_sub.size]) }
  elsif !type.dimensions.empty?
    l = type.dimensions.last[0]
    nested_sub = type.nested_sub

    # decoded static-size arrays
    return (0...l).map { |i| decode_type(nested_sub, arg[nested_sub.size * i, nested_sub.size]) }
  else

    # decoded primitive types
    return decode_primitive_type type, arg
  end
end

#encode(types, args) ⇒ String

Encodes Application Binary Interface (ABI) data. It accepts multiple arguments and encodes using the head/tail mechanism.

Parameters:

  • types (Array)

    types to be ABI-encoded.

  • args (Array)

    values to be ABI-encoded.

Returns:

  • (String)

    the encoded ABI data.



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/klay/abi.rb', line 44

def encode(types, args)

  # prase all types
  parsed_types = types.map { |t| Type.parse(t) }

  # prepare the "head"
  head_size = (0...args.size)
    .map { |i| parsed_types[i].size or 32 }
    .reduce(0, &:+)
  head, tail = "", ""

  # encode types and arguments
  args.each_with_index do |arg, i|
    if parsed_types[i].is_dynamic?
      head += encode_type Type.size_type, head_size + tail.size
      tail += encode_type parsed_types[i], arg
    else
      head += encode_type parsed_types[i], arg
    end
  end

  # return the encoded ABI blob
  return "#{head}#{tail}"
end

#encode_primitive_type(type, arg) ⇒ String

Encodes primitive types.

Parameters:

  • type (Klay::Abi::Type)

    type to be encoded.

  • arg (String|Number)

    value to be encoded.

Returns:

  • (String)

    the encoded primitive type.

Raises:



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/klay/abi.rb', line 119

def encode_primitive_type(type, arg)
  case type.base_type
  when "uint"
    return encode_uint arg, type
  when "bool"
    return encode_bool arg
  when "int"
    return encode_int arg, type
  when "ureal", "ufixed"
    return encode_ufixed arg, type
  when "real", "fixed"
    return encode_fixed arg, type
  when "string", "bytes"
    return encode_bytes arg, type
  when "hash"
    return encode_hash arg, type
  when "address"
    return encode_address arg
  else
    raise EncodingError, "Unhandled type: #{type.base_type} #{type.sub_type}"
  end
end

#encode_type(type, arg) ⇒ String

Encodes a specific value, either static or dynamic.

Parameters:

  • type (Klay::Abi::Type)

    type to be encoded.

  • arg (String|Number)

    value to be encoded.

Returns:

  • (String)

    the encoded type.

Raises:



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/klay/abi.rb', line 75

def encode_type(type, arg)
  if %w(string bytes).include? type.base_type and type.sub_type.empty?
    raise EncodingError, "Argument must be a String" unless arg.instance_of? String

    # encodes strings and bytes
    size = encode_type Type.size_type, arg.size
    padding = Constant::BYTE_ZERO * (Util.ceil32(arg.size) - arg.size)
    return "#{size}#{arg}#{padding}"
  elsif type.is_dynamic?
    raise EncodingError, "Argument must be an Array" unless arg.instance_of? Array

    # encodes dynamic-sized arrays
    head, tail = "", ""
    head += encode_type Type.size_type, arg.size
    nested_sub = type.nested_sub
    nested_sub_size = type.nested_sub.size
    arg.size.times do |i|

      # ref https://github.com/ethereum/tests/issues/691
      raise NotImplementedError, "Encoding dynamic arrays with nested dynamic sub-types is not implemented for ABI." if nested_sub.is_dynamic?
      head += encode_type nested_sub, arg[i]
    end
    return "#{head}#{tail}"
  else
    if type.dimensions.empty?

      # encode a primitive type
      return encode_primitive_type type, arg
    else

      # encode static-size arrays
      return arg.map { |x| encode_type(type.nested_sub, x) }.join
    end
  end
end