lib/eth/abi/type.rb
# Copyright (c) 2016-2023 The Ruby-Eth Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# -*- encoding : ascii-8bit -*-
# Provides the {Eth} module.
module Eth
# Provides a Ruby implementation of the Ethereum Application Binary Interface (ABI).
module Abi
# Provides a class to handle and parse common ABI types.
class Type
# Provides a specific parser error if type cannot be determined.
class ParseError < StandardError; end
# The base attribute, e.g., `string` or `bytes`.
attr :base_type
# The sub-type attribute, e.g., `256` as size of an uint256.
attr :sub_type
# The dimension attribute, e.g., `[10]` for an array of size 10.
attr :dimensions
# The components of a tuple type.
attr :components
# The name of tuple component.
attr :name
# Create a new Type object for base types, sub types, and dimensions.
# Should not be used; use {Type.parse} instead.
#
# @param base_type [String] the base-type attribute.
# @param sub_type [String] the sub-type attribute.
# @param dimensions [Array] the dimension attribute.
# @param components [Array] the components attribute.
# @param component_name [String] the tuple component's name.
# @return [Eth::Abi::Type] an ABI type object.
def initialize(base_type, sub_type, dimensions, components = nil, component_name = nil)
sub_type = sub_type.to_s
@base_type = base_type
@sub_type = sub_type
@dimensions = dimensions
@components = components
@name = component_name
end
# Converts the self.parse method into a constructor.
konstructor :parse
# Attempts to parse a string containing a common Solidity type.
# Creates a new Type upon success (using konstructor).
#
# @param type [String] a common Solidity type.
# @param components [Array] the components attribute.
# @param component_name [String] the tuple component's name.
# @return [Eth::Abi::Type] a parsed Type object.
# @raise [ParseError] if it fails to parse the type.
def parse(type, components = nil, component_name = nil)
if type.is_a?(Type)
@base_type = type.base_type
@sub_type = type.sub_type
@dimensions = type.dimensions
@components = type.components
@name = type.name
return
end
_, base_type, sub_type, dimension = /([a-z]*)([0-9]*x?[0-9]*)((\[[0-9]*\])*)/.match(type).to_a
# type dimension can only be numeric
dims = dimension.scan(/\[[0-9]*\]/)
raise ParseError, "Unknown characters found in array declaration" if dims.join != dimension
# enforce base types
validate_base_type base_type, sub_type
# return a new Type (using konstructor)
sub_type = sub_type.to_s
@base_type = base_type
@sub_type = sub_type
@dimensions = dims.map { |x| x[1...-1].to_i }
@components = components.map { |component| Abi::Type.parse(component["type"], component.dig("components"), component.dig("name")) } unless components.nil?
@name = component_name
end
# Creates a new uint256 type used for size.
#
# @return [Eth::Abi::Type] a uint256 size type.
def self.size_type
@size_type ||= new("uint", 256, [])
end
# Compares two types for their attributes.
#
# @param another_type [Eth::Abi::Type] another type to be compared.
# @return [Boolean] true if all attributes match.
def ==(another_type)
base_type == another_type.base_type and
sub_type == another_type.sub_type and
dimensions == another_type.dimensions
end
# Computes the size of a type if possible.
#
# @return [Integer] the size of the type; or nil if not available.
def size
s = nil
if dimensions.empty?
if !(["string", "bytes", "tuple"].include?(base_type) and sub_type.empty?)
s = 32
elsif base_type == "tuple" && components.none?(&:dynamic?)
s = components.sum(&:size)
end
elsif dimensions.last != 0 && !nested_sub.dynamic?
s = dimensions.last * nested_sub.size
end
@size ||= s
end
# Helpes to determine whether array is of dynamic size.
#
# @return [Boolean] true if array is of dynamic size.
def dynamic?
size.nil?
end
# Types can have nested sub-types in arrays.
#
# @return [Eth::Abi::Type] nested sub-type.
def nested_sub
@nested_sub ||= self.class.new(base_type, sub_type, dimensions[0...-1], components, name)
end
# Allows exporting the type as string.
#
# @return [String] the type string.
def to_s
if base_type == "tuple"
"(" + components.map(&:to_s).join(",") + ")" + (dimensions.size > 0 ? dimensions.map { |x| "[#{x == 0 ? "" : x}]" }.join : "")
elsif dimensions.empty?
if %w[string bytes].include?(base_type) && sub_type.empty?
base_type
else
"#{base_type}#{sub_type}"
end
else
"#{base_type}#{sub_type}#{dimensions.map { |x| "[#{x == 0 ? "" : x}]" }.join}"
end
end
private
# Validates all known base types and raises if an issue occurs.
def validate_base_type(base_type, sub_type)
case base_type
when "string"
# string can not have any suffix
raise ParseError, "String type must have no suffix or numerical suffix" unless sub_type.empty?
when "bytes"
# bytes can be no longer than 32 bytes
raise ParseError, "Maximum 32 bytes for fixed-length string or bytes" unless sub_type.empty? || sub_type.to_i <= 32
when "tuple"
# tuples can not have any suffix
raise ParseError, "Tuple type must have no suffix or numerical suffix" unless sub_type.empty?
when "uint", "int"
# integers must have a numerical suffix
raise ParseError, "Integer type must have numerical suffix" unless sub_type =~ /\A[0-9]+\z/
# integer size must be valid
size = sub_type.to_i
raise ParseError, "Integer size out of bounds" unless size >= 8 && size <= 256
raise ParseError, "Integer size must be multiple of 8" unless size % 8 == 0
when "ureal", "real", "fixed", "ufixed"
# floats must have valid dimensional suffix
raise ParseError, "Real type must have suffix of form <high>x<low>, e.g. 128x128" unless sub_type =~ /\A[0-9]+x[0-9]+\z/
high, low = sub_type.split("x").map(&:to_i)
total = high + low
raise ParseError, "Real size out of bounds (max 32 bytes)" unless total >= 8 && total <= 256
raise ParseError, "Real high/low sizes must be multiples of 8" unless high % 8 == 0 && low % 8 == 0
when "hash"
# hashs must have numerical suffix
raise ParseError, "Hash type must have numerical suffix" unless sub_type =~ /\A[0-9]+\z/
when "address"
# addresses cannot have any suffix
raise ParseError, "Address cannot have suffix" unless sub_type.empty?
when "bool"
# booleans cannot have any suffix
raise ParseError, "Bool cannot have suffix" unless sub_type.empty?
else
# we cannot parse arbitrary types such as 'decimal' or 'hex'
raise ParseError, "Unknown base type"
end
end
end
end
end