registerStructType method

LegacyBCS registerStructType(
  1. dynamic typeName,
  2. StructTypeDefinition fields
)

Safe method to register a custom Move struct. The first argument is a name of the struct which is only used on the FrontEnd and has no affect on serialization results, and the second is a struct description passed as an Object.

The description object MUST have the same order on all of the platforms (ie in Move or in Rust).

// Move / Rust struct
// struct Coin {
//   value: u64,
//   owner: vector<u8>, // name // Vec<u8> in Rust
//   is_locked: bool,
// }

bcs.registerStructType('Coin', {
  'value': BCS.U64,
  'owner': BCS.STRING,
  'is_locked': BCS.BOOL
});

// Created in Rust with diem/bcs
// const rust_bcs_str = '80d1b105600000000e4269672057616c6c65742047757900';
final rust_bcs_str = [ // using an Array here as BCS works with Uint8List
 128, 209, 177,   5,  96,  0,  0,
   0,  14,  66, 105, 103, 32, 87,
  97, 108, 108, 101, 116, 32, 71,
 117, 121,   0
];

// Let's encode the value as well
final test_set = bcs.ser('Coin', {
  'owner': 'Big Wallet Guy',
  'value': '412412400000',
  'is_locked': false,
});

expect(test_set.toBytes(), rust_bcs_str);

Implementation

LegacyBCS registerStructType(TypeName typeName, StructTypeDefinition fields) {
  // When an Object is passed, we register it under a new key and store it
  // in the registered type system. This way we allow nested inline definitions.
  final fieldsTmp = <String, dynamic>{}; // fix dynamic change value type of Map
  for (final key in fields.keys) {
    final internalName = tempKey();
    final value = fields[key];

    // TODO: add a type guard here?
    if (value is! String && value is! Iterable) {
      fieldsTmp[key] = internalName;
      registerStructType(internalName, value as StructTypeDefinition);
    } else {
      fieldsTmp[key] = value;
    }
  }
  fields = fieldsTmp;

  // Make sure the order doesn't get changed
  final struct = Map<String, dynamic>.from(fields);

  // IMPORTANT: we need to store canonical order of fields for each registered
  // struct so we maintain it and allow developers to use any field ordering in
  // their code (and not cause mismatches based on field order).
  final canonicalOrder = struct.keys;

  // Holds generics for the struct definition. At this stage we can check that
  // generic parameter matches the one defined in the struct.
  final (structName, generics) = parseTypeName(typeName);

  // Make sure all the types in the fields description are already known
  // and that all the field types are strings.
  return registerType(typeName, (writer, data, typeParams, typeMap) {
    if (data == null) {
      throw ArgumentError("Expected $structName to be an Object, got: $data");
    }

    if (typeParams.length != generics.length) {
      throw ArgumentError(
          "Incorrect number of generic parameters passed; expected: ${generics.length}, got: ${typeParams.length}");
    }

    if (data is! Map) {
      data = data.toJson();
    }

    // follow the canonical order when serializing
    for (String key in canonicalOrder) {
      if (!data.containsKey(key)) {
        throw Exception('Struct $structName requires field $key:${data[key]}');
      }

      // Before deserializing, read the canonical field type.
      final (fieldType, fieldParams) = parseTypeName(struct[key] as TypeName);

      // Check whether this type is a generic defined in this struct.
      // If it is -> read the type parameter matching its index.
      // If not - tread as a regular field.
      if (!generics.contains(fieldType)) {
        getTypeInterface(fieldType).encodeRaw(writer, data[key], fieldParams, typeMap);
      } else {
        final paramIdx = generics.indexOf(fieldType);
        final (name, params) = parseTypeName(typeParams[paramIdx]);

        // If the type from the type parameters already exists
        // and known -> proceed with type decoding.
        if (hasType(name)) {
          getTypeInterface(name).encodeRaw(writer, data[key], params, typeMap);
          continue;
        }

        // Alternatively, if it's a global generic parameter...
        if (!(typeMap.containsKey(name))) {
          throw ArgumentError(
              "Unable to find a matching type definition for ${name} in ${structName}; make sure you passed a generic");
        }

        final (innerName, innerParams) = parseTypeName(typeMap[name]);
        getTypeInterface(innerName).encodeRaw(writer, data[key], innerParams, typeMap);
      }
    }
    return writer;
  }, (reader, typeParams, typeMap) {
    if (typeParams.length != generics.length) {
      throw ArgumentError(
          "Incorrect number of generic parameters passed; expected: ${generics.length}, got: ${typeParams.length}");
    }

    final result = <String, dynamic>{};
    for (String key in canonicalOrder) {
      final (fieldName, fieldParams) = parseTypeName(struct[key] as TypeName);

      // if it's not a generic
      if (!generics.contains(fieldName)) {
        result[key] = getTypeInterface(fieldName).decodeRaw(reader, fieldParams, typeMap);
      } else {
        final paramIdx = generics.indexOf(fieldName);
        final (name, params) = parseTypeName(typeParams[paramIdx]);

        // If the type from the type parameters already exists
        // and known -> proceed with type decoding.
        if (hasType(name)) {
          result[key] = getTypeInterface(name).decodeRaw(reader, params, typeMap);
          continue;
        }

        if (!(typeMap.containsKey(name))) {
          throw ArgumentError(
              "Unable to find a matching type definition for ${name} in ${structName}; make sure you passed a generic");
        }

        final (innerName, innerParams) = parseTypeName(typeMap[name]);
        result[key] = getTypeInterface(innerName).decodeRaw.call(reader, innerParams, typeMap);
      }
    }
    return result;
  });
}