///| Decoding a compiler ABI array into the ABI model.
///
/// Decoding validates and canonicalizes: a signature is built from canonical
/// parameter types, and a selector or topic is derived from that signature, so
/// comparison never depends on how the compiler happened to print a type or on
/// which order the JSON keys appeared in.
///|
/// Decodes an `abi` JSON array into the ABI model.
///
/// Returns `None` when the value is `null`, which stands for an artifact that
/// carries no ABI, and also after reporting a value that is not an array.
/// Entries that cannot be decoded are reported and skipped, so an ABI that
/// comes back with a diagnostic is incomplete and must not be compared as if
/// it were whole.
fn decode_abi(
json : Json,
path : String,
diagnostics : Array[Diagnostic],
) -> Abi? {
let entries = match json {
Array(entries) => entries
Null => return None
_ => {
diagnostics.push(invalid_field(path, "expected a JSON array"))
return None
}
}
let abi : Abi = Abi::empty()
for i in 0.. value
None | Some(Null) => {
diagnostics.push(
invalid_field(entry_path + ".type", "required field is missing"),
)
continue
}
Some(_) => {
diagnostics.push(
invalid_field(entry_path + ".type", "expected a JSON string"),
)
continue
}
}
match kind {
"function" =>
match decode_callable(fields, entry_path, Function, diagnostics) {
Some(entry) => abi.functions.push(entry)
None => ()
}
"fallback" =>
match decode_callable(fields, entry_path, Fallback, diagnostics) {
Some(entry) => abi.functions.push(entry)
None => ()
}
"receive" =>
match decode_callable(fields, entry_path, Receive, diagnostics) {
Some(entry) => abi.functions.push(entry)
None => ()
}
"event" =>
match decode_event(fields, entry_path, diagnostics) {
Some(entry) => abi.events.push(entry)
None => ()
}
"error" =>
match decode_error(fields, entry_path, diagnostics) {
Some(entry) => abi.errors.push(entry)
None => ()
}
// Constructor inputs affect deployment, not the interface an existing
// proxy exposes, so they take no part in compatibility.
"constructor" => ()
other =>
diagnostics.push(
invalid_field(
entry_path + ".type",
"unsupported ABI entry type \"\{other}\"",
),
)
}
}
Some(abi)
}
///|
/// Decodes one callable entry: a function, fallback, or receive.
fn decode_callable(
fields : Map[String, Json],
path : String,
kind : AbiEntryKind,
diagnostics : Array[Diagnostic],
) -> AbiFunction? {
let name = match kind {
Function => {
guard require_string(fields, "name", path, diagnostics) is Some(value) else {
return None
}
value
}
Fallback | Receive => ""
}
guard require_mutability(fields, path, diagnostics) is Some(mutability) else {
return None
}
let inputs = if kind == Function {
match decode_params(fields, "inputs", path, false, diagnostics) {
Some(inputs) => inputs
None => return None
}
} else {
[]
}
let outputs = if kind == Function {
match decode_params(fields, "outputs", path, false, diagnostics) {
Some(outputs) => outputs
None => return None
}
} else {
[]
}
let signature = canonical_signature(callable_name(name, kind), inputs)
Some({
kind,
name,
signature,
selector: selector_of(signature),
inputs,
outputs,
mutability,
})
}
///|
/// The name an entry contributes to its signature.
///
/// Fallback and receive carry no name in the ABI, but their signatures are
/// still written `fallback()` and `receive()`, which is what tooling hashes and
/// prints.
fn callable_name(name : String, kind : AbiEntryKind) -> String {
match kind {
Function => name
Fallback => "fallback"
Receive => "receive"
}
}
///|
/// Decodes one event.
fn decode_event(
fields : Map[String, Json],
path : String,
diagnostics : Array[Diagnostic],
) -> AbiEvent? {
guard require_string(fields, "name", path, diagnostics) is Some(name) else {
return None
}
guard decode_params(fields, "inputs", path, true, diagnostics) is Some(inputs) else {
return None
}
let anonymous = match fields.get("anonymous") {
Some(True) => true
Some(False) => false
None | Some(Null) => {
diagnostics.push(
invalid_field(path + ".anonymous", "required field is missing"),
)
return None
}
Some(_) => {
diagnostics.push(invalid_field(path + ".anonymous", "expected a boolean"))
return None
}
}
let signature = canonical_signature(name, inputs)
Some({ name, signature, topic: topic_of(signature), inputs, anonymous, })
}
///|
/// Decodes one custom error.
fn decode_error(
fields : Map[String, Json],
path : String,
diagnostics : Array[Diagnostic],
) -> AbiError? {
guard require_string(fields, "name", path, diagnostics) is Some(name) else {
return None
}
guard decode_params(fields, "inputs", path, false, diagnostics)
is Some(inputs) else {
return None
}
let signature = canonical_signature(name, inputs)
Some({ name, signature, selector: selector_of(signature), inputs, })
}
///|
/// Decodes a required parameter list field.
fn decode_params(
fields : Map[String, Json],
name : String,
path : String,
indexed_required : Bool,
diagnostics : Array[Diagnostic],
) -> Array[AbiParam]? {
match fields.get(name) {
None | Some(Null) => {
diagnostics.push(
invalid_field(path + "." + name, "required field is missing"),
)
None
}
Some(Array(items)) => {
let params : Array[AbiParam] = []
for i in 0.. {
diagnostics.push(
invalid_field(path + "." + name, "expected a JSON array"),
)
None
}
}
}
///|
/// Decodes one parameter, including its canonical type name.
fn decode_param(
json : Json,
path : String,
indexed_required : Bool,
diagnostics : Array[Diagnostic],
) -> AbiParam? {
guard json is Object(fields) else {
diagnostics.push(invalid_field(path, "expected a JSON object"))
return None
}
guard require_string(fields, "type", path, diagnostics) is Some(type_name) else {
return None
}
guard canonical_type(type_name, fields.get("components"), path, diagnostics)
is Some(canonical) else {
return None
}
let name = match fields.get("name") {
Some(String(value)) => value
_ => ""
}
let indexed = if indexed_required {
match fields.get("indexed") {
Some(True) => true
Some(False) => false
None | Some(Null) => {
diagnostics.push(
invalid_field(path + ".indexed", "required field is missing"),
)
return None
}
Some(_) => {
diagnostics.push(invalid_field(path + ".indexed", "expected a boolean"))
return None
}
}
} else {
false
}
Some({ name, type_name: canonical, indexed, })
}
///|
/// The canonical type name of a parameter.
///
/// Solidity's ABI specification canonicalizes tuples as `(t1,t2)`, and every
/// parameter has to be written that way for its selector to match the one
/// callers compute. `type` is therefore checked against the canonical
/// elementary names instead of being trusted, and a name that is not canonical
/// is rejected rather than silently hashed into a wrong selector.
fn canonical_type(
type_name : String,
components : Json?,
path : String,
diagnostics : Array[Diagnostic],
) -> String? {
let (base, suffix) = split_array_suffix(type_name)
if !valid_array_suffix(suffix) {
diagnostics.push(
invalid_field(path + ".type", "unsupported array type \"\{type_name}\""),
)
return None
}
if base == "tuple" {
match components {
Some(Array(items)) => {
let parts : Array[String] = []
for i in 0.. {
diagnostics.push(
invalid_field(path + ".components", "a tuple type needs components"),
)
None
}
Some(_) => {
diagnostics.push(
invalid_field(path + ".components", "expected a JSON array"),
)
None
}
}
} else if is_elementary_type(base) {
Some(base + suffix)
} else {
diagnostics.push(
invalid_field(path + ".type", "unsupported ABI type \"\{type_name}\""),
)
None
}
}
///|
/// Splits a type name into its base name and its array suffixes.
fn split_array_suffix(type_name : String) -> (String, String) {
match type_name.find("[") {
Some(index) => (type_name[:index].to_owned(), type_name[index:].to_owned())
None => (type_name, "")
}
}
///|
/// Whether an array suffix is a sequence of `[length]` groups, where `length`
/// may be empty for a dynamic array.
fn valid_array_suffix(suffix : String) -> Bool {
let chars = suffix.to_array()
let mut i = 0
while i < chars.length() {
if chars[i] != '[' {
return false
}
i += 1
while i < chars.length() && chars[i].is_ascii_digit() {
i += 1
}
if i >= chars.length() || chars[i] != ']' {
return false
}
i += 1
}
true
}
///|
/// Whether `name` is a canonical elementary ABI type.
fn is_elementary_type(name : String) -> Bool {
match name {
"address" | "bool" | "string" | "bytes" | "function" => true
_ =>
if name.has_prefix("uint") {
sized_integer(name, 4, 8)
} else if name.has_prefix("int") {
sized_integer(name, 3, 8)
} else if name.has_prefix("bytes") {
sized_bytes(name)
} else {
false
}
}
}
///|
/// Whether the width in a `uintN` or `intN` name is a valid multiple of 8 up
/// to 256.
fn sized_integer(name : String, prefix_length : Int, step : Int) -> Bool {
match parse_decimal_int(name[prefix_length:].to_owned()) {
Some(size) => size >= step && size <= 256 && size % step == 0
None => false
}
}
///|
/// Whether the width in a `bytesN` name is between 1 and 32.
fn sized_bytes(name : String) -> Bool {
match parse_decimal_int(name[5:].to_owned()) {
Some(size) => size >= 1 && size <= 32
None => false
}
}
///|
/// Reads a required string field, reporting whether it is absent or of the
/// wrong JSON type.
fn require_string(
fields : Map[String, Json],
name : String,
path : String,
diagnostics : Array[Diagnostic],
) -> String? {
match fields.get(name) {
Some(String(value)) => Some(value)
None | Some(Null) => {
diagnostics.push(
invalid_field(path + "." + name, "required field is missing"),
)
None
}
Some(_) => {
diagnostics.push(
invalid_field(path + "." + name, "expected a JSON string"),
)
None
}
}
}
///|
/// Reads the required `stateMutability` field.
///
/// Pre-0.5 ABIs wrote `constant` and `payable` booleans instead. They cannot be
/// translated exactly, because `constant` does not distinguish `pure` from
/// `view`, so they are reported as unsupported rather than guessed at.
fn require_mutability(
fields : Map[String, Json],
path : String,
diagnostics : Array[Diagnostic],
) -> Mutability? {
match fields.get("stateMutability") {
Some(String(value)) =>
match value {
"pure" => Some(Pure)
"view" => Some(View)
"nonpayable" => Some(NonPayable)
"payable" => Some(Payable)
other => {
diagnostics.push(
invalid_field(
path + ".stateMutability",
"unsupported state mutability \"\{other}\"",
),
)
None
}
}
None | Some(Null) =>
if fields.contains("constant") || fields.contains("payable") {
diagnostics.push(
invalid_field(
path + ".stateMutability",
"legacy ABI mutability fields are not supported; recompile with Solidity 0.5 or later",
),
)
None
} else {
diagnostics.push(
invalid_field(path + ".stateMutability", "required field is missing"),
)
None
}
Some(_) => {
diagnostics.push(
invalid_field(path + ".stateMutability", "expected a JSON string"),
)
None
}
}
}
///|
/// Canonical signature of an entry: its name and canonical parameter types.
fn canonical_signature(name : String, params : Array[AbiParam]) -> String {
let types : Array[String] = params.map(fn(param) { param.type_name })
name + "(" + types.join(",") + ")"
}
///|
/// Four-byte selector of a signature, as `0x` plus eight lowercase hex digits.
fn selector_of(signature : String) -> String {
"0x" + hex_encode(keccak_256(@utf8.encode(signature)))[:8].to_owned()
}
///|
/// Topic of an event signature, as `0x` plus 64 lowercase hex digits.
fn topic_of(signature : String) -> String {
"0x" + hex_encode(keccak_256(@utf8.encode(signature)))
}