LoginSignup
8
4

Symbol 各言語のSDKを作成する -その2-

Last updated at Posted at 2024-02-01

この記事の続き

StructTypeFormatter

さて、これが一番大変です。トランザクションやMosaicなど、フィールドが単純ではないオリジナルのモデルを活用しているStructTypeのフォーマッターです。

例えばpythonだとこんなの

class TransferTransactionV1:
	TRANSACTION_VERSION: int = 1
	TRANSACTION_TYPE: TransactionType = TransactionType.TRANSFER
	TYPE_HINTS = {
		'signature': 'pod:Signature',
		'signer_public_key': 'pod:PublicKey',
		'network': 'enum:NetworkType',
		'type_': 'enum:TransactionType',
		'fee': 'pod:Amount',
		'deadline': 'pod:Timestamp',
		'recipient_address': 'pod:UnresolvedAddress',
		'mosaics': 'array[UnresolvedMosaic]',
		'message': 'bytes_array'
	}

	def __init__(self):
		self._signature = Signature()
		self._signer_public_key = PublicKey()
		self._version = TransferTransactionV1.TRANSACTION_VERSION
		self._network = NetworkType.MAINNET
		self._type_ = TransferTransactionV1.TRANSACTION_TYPE
		self._fee = Amount()
		self._deadline = Timestamp()
		self._recipient_address = UnresolvedAddress()
		self._mosaics = []
		self._message = bytes()
		self._verifiable_entity_header_reserved_1 = 0  # reserved field
		self._entity_body_reserved_1 = 0  # reserved field
		self._transfer_transaction_body_reserved_1 = 0  # reserved field
		self._transfer_transaction_body_reserved_2 = 0  # reserved field

	def sort(self) -> None:
		self._mosaics = sorted(self._mosaics, key=lambda e: e.mosaic_id.comparer() if hasattr(e.mosaic_id, 'comparer') else e.mosaic_id)

	@property
	def signature(self) -> Signature:
		return self._signature

	@property
	def signer_public_key(self) -> PublicKey:
		return self._signer_public_key

	@property
	def version(self) -> int:
		return self._version

	@property
	def network(self) -> NetworkType:
		return self._network

	@property
	def type_(self) -> TransactionType:
		return self._type_

	@property
	def fee(self) -> Amount:
		return self._fee

	@property
	def deadline(self) -> Timestamp:
		return self._deadline

	@property
	def recipient_address(self) -> UnresolvedAddress:
		return self._recipient_address

	@property
	def mosaics(self) -> List[UnresolvedMosaic]:
		return self._mosaics

	@property
	def message(self) -> bytes:
		return self._message

	@signature.setter
	def signature(self, value: Signature):
		self._signature = value

	@signer_public_key.setter
	def signer_public_key(self, value: PublicKey):
		self._signer_public_key = value

	@version.setter
	def version(self, value: int):
		self._version = value

	@network.setter
	def network(self, value: NetworkType):
		self._network = value

	@type_.setter
	def type_(self, value: TransactionType):
		self._type_ = value

	@fee.setter
	def fee(self, value: Amount):
		self._fee = value

	@deadline.setter
	def deadline(self, value: Timestamp):
		self._deadline = value

	@recipient_address.setter
	def recipient_address(self, value: UnresolvedAddress):
		self._recipient_address = value

	@mosaics.setter
	def mosaics(self, value: List[UnresolvedMosaic]):
		self._mosaics = value

	@message.setter
	def message(self, value: bytes):
		self._message = value

	@property
	def size(self) -> int:
		size = 0
		size += 4
		size += 4
		size += self.signature.size
		size += self.signer_public_key.size
		size += 4
		size += 1
		size += self.network.size
		size += self.type_.size
		size += self.fee.size
		size += self.deadline.size
		size += self.recipient_address.size
		size += 2
		size += 1
		size += 1
		size += 4
		size += ArrayHelpers.size(self.mosaics)
		size += len(self._message)
		return size

	@classmethod
	def deserialize(cls, payload: ByteString) -> TransferTransactionV1:
		buffer = memoryview(payload)
		size_ = int.from_bytes(buffer[:4], byteorder='little', signed=False)
		buffer = buffer[4:]
		buffer = buffer[:size_ - 4]
		del size_
		verifiable_entity_header_reserved_1 = int.from_bytes(buffer[:4], byteorder='little', signed=False)
		buffer = buffer[4:]
		assert verifiable_entity_header_reserved_1 == 0, f'Invalid value of reserved field ({verifiable_entity_header_reserved_1})'
		signature = Signature.deserialize(buffer)
		buffer = buffer[signature.size:]
		signer_public_key = PublicKey.deserialize(buffer)
		buffer = buffer[signer_public_key.size:]
		entity_body_reserved_1 = int.from_bytes(buffer[:4], byteorder='little', signed=False)
		buffer = buffer[4:]
		assert entity_body_reserved_1 == 0, f'Invalid value of reserved field ({entity_body_reserved_1})'
		version = int.from_bytes(buffer[:1], byteorder='little', signed=False)
		buffer = buffer[1:]
		network = NetworkType.deserialize(buffer)
		buffer = buffer[network.size:]
		type_ = TransactionType.deserialize(buffer)
		buffer = buffer[type_.size:]
		fee = Amount.deserialize(buffer)
		buffer = buffer[fee.size:]
		deadline = Timestamp.deserialize(buffer)
		buffer = buffer[deadline.size:]
		recipient_address = UnresolvedAddress.deserialize(buffer)
		buffer = buffer[recipient_address.size:]
		message_size = int.from_bytes(buffer[:2], byteorder='little', signed=False)
		buffer = buffer[2:]
		mosaics_count = int.from_bytes(buffer[:1], byteorder='little', signed=False)
		buffer = buffer[1:]
		transfer_transaction_body_reserved_1 = int.from_bytes(buffer[:1], byteorder='little', signed=False)
		buffer = buffer[1:]
		assert transfer_transaction_body_reserved_1 == 0, f'Invalid value of reserved field ({transfer_transaction_body_reserved_1})'
		transfer_transaction_body_reserved_2 = int.from_bytes(buffer[:4], byteorder='little', signed=False)
		buffer = buffer[4:]
		assert transfer_transaction_body_reserved_2 == 0, f'Invalid value of reserved field ({transfer_transaction_body_reserved_2})'
		mosaics = ArrayHelpers.read_array_count(buffer, UnresolvedMosaic, mosaics_count, lambda e: e.mosaic_id.comparer() if hasattr(e.mosaic_id, 'comparer') else e.mosaic_id)
		buffer = buffer[ArrayHelpers.size(mosaics):]
		message = ArrayHelpers.get_bytes(buffer, message_size)
		buffer = buffer[message_size:]

		instance = TransferTransactionV1()
		instance._signature = signature
		instance._signer_public_key = signer_public_key
		instance._version = version
		instance._network = network
		instance._type_ = type_
		instance._fee = fee
		instance._deadline = deadline
		instance._recipient_address = recipient_address
		instance._mosaics = mosaics
		instance._message = message
		return instance

	def serialize(self) -> bytes:
		buffer = bytes()
		buffer += self.size.to_bytes(4, byteorder='little', signed=False)
		buffer += self._verifiable_entity_header_reserved_1.to_bytes(4, byteorder='little', signed=False)
		buffer += self._signature.serialize()
		buffer += self._signer_public_key.serialize()
		buffer += self._entity_body_reserved_1.to_bytes(4, byteorder='little', signed=False)
		buffer += self._version.to_bytes(1, byteorder='little', signed=False)
		buffer += self._network.serialize()
		buffer += self._type_.serialize()
		buffer += self._fee.serialize()
		buffer += self._deadline.serialize()
		buffer += self._recipient_address.serialize()
		buffer += len(self._message).to_bytes(2, byteorder='little', signed=False)  # message_size
		buffer += len(self._mosaics).to_bytes(1, byteorder='little', signed=False)  # mosaics_count
		buffer += self._transfer_transaction_body_reserved_1.to_bytes(1, byteorder='little', signed=False)
		buffer += self._transfer_transaction_body_reserved_2.to_bytes(4, byteorder='little', signed=False)
		buffer += ArrayHelpers.write_array(self._mosaics, lambda e: e.mosaic_id.comparer() if hasattr(e.mosaic_id, 'comparer') else e.mosaic_id)
		buffer += self._message
		return buffer

	def __str__(self) -> str:
		result = '('
		result += f'signature: {self._signature.__str__()}, '
		result += f'signer_public_key: {self._signer_public_key.__str__()}, '
		result += f'version: 0x{self._version:X}, '
		result += f'network: {self._network.__str__()}, '
		result += f'type_: {self._type_.__str__()}, '
		result += f'fee: {self._fee.__str__()}, '
		result += f'deadline: {self._deadline.__str__()}, '
		result += f'recipient_address: {self._recipient_address.__str__()}, '
		result += f'mosaics: {list(map(str, self._mosaics))}, '
		result += f'message: {hexlify(self._message).decode("utf8")}, '
		result += ')'
		return result

これをdartでは

class TransferTransactionV1 implements ISerializable, ITransaction {
  static const int TRANSACTION_VERSION = 1;
  static final TransactionType TRANSACTION_TYPE = TransactionType(TransactionType.TRANSFER.value);

  static const Map<String, String> TYPE_HINTS = {
  	'signature': 'pod:Signature',
  	'signerPublicKey': 'pod:PublicKey',
  	'network': 'enum:NetworkType',
  	'type': 'enum:TransactionType',
  	'fee': 'pod:Amount',
  	'deadline': 'pod:Timestamp',
  	'recipientAddress': 'pod:UnresolvedAddress',
  	'mosaics': 'array[UnresolvedMosaic]',
  	'message': 'bytes_array'
  };

  @override
  late Signature signature = Signature();
  @override
  late PublicKey signerPublicKey = PublicKey();
  @override
  late int version;
  late NetworkType network = NetworkType.MAINNET;
  @override
  late TransactionType type;
  late Amount fee = Amount();
  late Timestamp deadline = Timestamp();
  late UnresolvedAddress recipientAddress = UnresolvedAddress();
  late List<UnresolvedMosaic> mosaics = [];
  late Uint8List message = Uint8List(0);
  final int verifiableEntityHeaderReserved_1 = 0; // reserved field
  final int entityBodyReserved_1 = 0; // reserved field
  final int transferTransactionBodyReserved_1 = 0; // reserved field
  final int transferTransactionBodyReserved_2 = 0; // reserved field

  TransferTransactionV1({ 
  Signature? signature,
  PublicKey? signerPublicKey,
  int? version,
  NetworkType? network,
  TransactionType? type,
  Amount? fee,
  Timestamp? deadline,
  UnresolvedAddress? recipientAddress,
  List<UnresolvedMosaic>? mosaics,
  Uint8List? message
  }) {
  	this.signature = signature ?? Signature();
  	this.signerPublicKey = signerPublicKey ?? PublicKey();
  	this.version = version ?? TransferTransactionV1.TRANSACTION_VERSION;
  	this.network = network ?? NetworkType.MAINNET;
  	this.type = type ?? TransferTransactionV1.TRANSACTION_TYPE;
  	this.fee = fee ?? Amount();
  	this.deadline = deadline ?? Timestamp();
  	this.recipientAddress = recipientAddress ?? UnresolvedAddress();
  	this.mosaics = mosaics ?? [];
  	this.message = message ?? Uint8List(0);
  }

  @override
  void sort() {
  	mosaics.sort((lhs, rhs) {
  		return ArrayHelpers.deepCompare(ArrayHelpers.getValue(lhs.mosaicId), ArrayHelpers.getValue(rhs.mosaicId));
  	});
  }

  @override
  int get size {
  	var size = 0;
  	size += 4;
  	size += 4;
  	size += signature.size;
  	size += signerPublicKey.size;
  	size += 4;
  	size += 1;
  	size += network.size;
  	size += type.size;
  	size += fee.size;
  	size += deadline.size;
  	size += recipientAddress.size;
  	size += 2;
  	size += 1;
  	size += 1;
  	size += 4;
  	size += ArrayHelpers.size(mosaics);
  	size += message.lengthInBytes;
  	return size;
  }

  @override
  TransferTransactionV1 deserialize(Uint8List payload) {
  	var buffer = payload;
  	var size = bytesToInt(buffer.sublist(0, 4), 4);
  	buffer = buffer.sublist(0, size);
  	buffer = buffer.sublist(4);
  	var verifiableEntityHeaderReserved_1 = bytesToInt(buffer.sublist(0, 4), 4);
  	buffer = buffer.sublist(4);
  	if (0 != verifiableEntityHeaderReserved_1) {
  		throw RangeError('Invalid value of reserved field ($verifiableEntityHeaderReserved_1)');
  	}
  	var signature = Signature().deserialize(buffer);
  	buffer = buffer.sublist(signature.size);
  	var signerPublicKey = PublicKey().deserialize(buffer);
  	buffer = buffer.sublist(signerPublicKey.size);
  	var entityBodyReserved_1 = bytesToInt(buffer.sublist(0, 4), 4);
  	buffer = buffer.sublist(4);
  	if (0 != entityBodyReserved_1) {
  		throw RangeError('Invalid value of reserved field ($entityBodyReserved_1)');
  	}
  	var version = bytesToInt(buffer.sublist(0, 1), 1);
  	buffer = buffer.sublist(1);
  	var network = NetworkType().deserialize(buffer);
  	buffer = buffer.sublist(network.size);
  	var type = TransactionType().deserialize(buffer);
  	buffer = buffer.sublist(type.size);
  	var fee = Amount().deserialize(buffer);
  	buffer = buffer.sublist(fee.size);
  	var deadline = Timestamp().deserialize(buffer);
  	buffer = buffer.sublist(deadline.size);
  	var recipientAddress = UnresolvedAddress().deserialize(buffer);
  	buffer = buffer.sublist(recipientAddress.size);
  	var messageSize = bytesToInt(buffer.sublist(0, 2), 2);
  	buffer = buffer.sublist(2);
  	var mosaicsCount = bytesToInt(buffer.sublist(0, 1), 1);
  	buffer = buffer.sublist(1);
  	var transferTransactionBodyReserved_1 = bytesToInt(buffer.sublist(0, 1), 1);
  	buffer = buffer.sublist(1);
  	if (0 != transferTransactionBodyReserved_1) {
  		throw RangeError('Invalid value of reserved field ($transferTransactionBodyReserved_1)');
  	}
  	var transferTransactionBodyReserved_2 = bytesToInt(buffer.sublist(0, 4), 4);
  	buffer = buffer.sublist(4);
  	if (0 != transferTransactionBodyReserved_2) {
  		throw RangeError('Invalid value of reserved field ($transferTransactionBodyReserved_2)');
  	}
  	var mosaics = ArrayHelpers.readArrayCount(buffer, UnresolvedMosaic(), mosaicsCount, (e) { return ArrayHelpers.getValue(e.mosaicId);}).map((item) => item as UnresolvedMosaic).toList();
  	buffer = buffer.sublist(ArrayHelpers.size(mosaics));
  	var message = Uint8List.fromList(buffer.sublist(0, messageSize));
  	buffer = buffer.sublist(messageSize);

  	var instance = TransferTransactionV1(
  		signature: signature,
  		signerPublicKey: signerPublicKey,
  		version: version,
  		network: network,
  		type: type,
  		fee: fee,
  		deadline: deadline,
  		recipientAddress: recipientAddress,
  		mosaics: mosaics,
  		message: message,
  	);
  	return instance;
  }

  @override
  Uint8List serialize() {
  	var buffer = Uint8List(size);
  	var currentPos = 0;
  	buffer.setRange(currentPos, currentPos + 4, intToBytes(size, 4));
  	currentPos += 4;
  	buffer.setRange(currentPos, currentPos + 4, intToBytes(verifiableEntityHeaderReserved_1, 4));
  	currentPos += 4;
  	buffer.setRange(currentPos, currentPos + signature.size, signature.serialize());
  	currentPos += signature.size;
  	buffer.setRange(currentPos, currentPos + signerPublicKey.size, signerPublicKey.serialize());
  	currentPos += signerPublicKey.size;
  	buffer.setRange(currentPos, currentPos + 4, intToBytes(entityBodyReserved_1, 4));
  	currentPos += 4;
  	buffer.setRange(currentPos, currentPos + 1, intToBytes(version, 1));
  	currentPos += 1;
  	buffer.setRange(currentPos, currentPos + network.size, network.serialize());
  	currentPos += network.size;
  	buffer.setRange(currentPos, currentPos + type.size, type.serialize());
  	currentPos += type.size;
  	buffer.setRange(currentPos, currentPos + fee.size, fee.serialize());
  	currentPos += fee.size;
  	buffer.setRange(currentPos, currentPos + deadline.size, deadline.serialize());
  	currentPos += deadline.size;
  	buffer.setRange(currentPos, currentPos + recipientAddress.size, recipientAddress.serialize());
  	currentPos += recipientAddress.size;
  	buffer.setRange(currentPos, currentPos + 2, intToBytes(message.length, 2));
  	currentPos += 2;
  	buffer.setRange(currentPos, currentPos + 1, intToBytes(mosaics.length, 1));
  	currentPos += 1;
  	buffer.setRange(currentPos, currentPos + 1, intToBytes(transferTransactionBodyReserved_1, 1));
  	currentPos += 1;
  	buffer.setRange(currentPos, currentPos + 4, intToBytes(transferTransactionBodyReserved_2, 4));
  	currentPos += 4;
  	sort();
  	var res_mosaics = ArrayHelpers.writeArray(buffer, mosaics, currentPos, (e) { return ArrayHelpers.getValue(e.mosaicId);});
  	currentPos = res_mosaics.item2;
  	buffer = res_mosaics.item1;
  	buffer.setRange(currentPos, currentPos + message.lengthInBytes, message);
  	currentPos += message.lengthInBytes;
  	return buffer;
  }

  @override
  String toString() {
  	var result = 'TransferTransactionV1(';
  	result += 'signature: "${signature.toString()}", ';
  	result += 'signerPublicKey: "${signerPublicKey.toString()}", ';
  	result += 'version: "0x${intToHex(version)}", ';
  	result += 'network: "${network.toString()}", ';
  	result += 'type: "${type.toString()}", ';
  	result += 'fee: "${fee.toString()}", ';
  	result += 'deadline: "${deadline.toString()}", ';
  	result += 'recipientAddress: "${recipientAddress.toString()}", ';
  	result += 'mosaics: "${mosaics.map((e) => e.toString()).toList()}", ';
  	result += 'message: "${bytesToHex(message)}", ';
  	result += ')';
  	return result;
  }
}

こうします。(作成しながら何度も変わってます。結局公開できてからこの記事を書くことにしました。)
例のごとくStructTypeFormatterから始めます。

ちなみにこのSDKはSymbolとNEMの両対応を前提としています。ただ、NEMに対応するために色々と修正すべき点が増えるので、本記事ではSymbolに関してのみにしようと思います。今後、要望があればNEMにも対応していきます。

def is_transaction(self):
    return True if 'Transaction' in self.struct.name and 'Statement' not in self.struct.name and not self.is_inner_transaction() else False

def is_inner_transaction(self):
    return True if 'Embedded' in self.struct.name or 'NonVerifiable' in self.struct.name else False

def get_interface(self):
    return ', ITransaction' if self.is_transaction() else ', IInnerTransaction' if self.is_inner_transaction() else ''

Structタイプはトランザクションとそうでないものがあります。言語によっては継承などが必要になるので、dartではStructタイプのクラス名にTransactionが含まれていればITransactionを、Embeddedが含まれていればIInnerTransactionを継承するようにしました。

/symbol/sdk/dart/symbol_sdk/lib/models/IInnerTransaction.dart
import 'dart:typed_data';
import '../models/ISerializable.dart';

interface class IInnerTransaction implements ISerializable {
  @override
  Uint8List serialize(){
    throw UnimplementedError('unimplement serialize');
  }
  @override
  dynamic deserialize(Uint8List payload){
    throw UnimplementedError('unimplement deserialize');
  }
  @override
  int get size{
    throw UnimplementedError('unimplement size');
  }
  void sort(){
    throw UnimplementedError('unimplement sort');
  }
}
/symbol/sdk/dart/symbol_sdk/lib/symbol/ITransaction.dart
import '../models/IInnerTransaction.dart';
import './models.dart';
import 'dart:typed_data';

interface class ITransaction implements IInnerTransaction {
  Signature signature = Signature();
  PublicKey signerPublicKey = PublicKey();
  TransactionType type = TransactionType.TRANSFER;
  int version = 0;

  @override
  int get size{
    throw UnimplementedError('unimplement size');
  }
  @override
  dynamic deserialize(Uint8List payload){
    throw UnimplementedError('unimplement deserialize');
  }
  @override
  Uint8List serialize(){
    throw UnimplementedError('unimplement serialize');
  }
  @override
  void sort(){
    throw UnimplementedError('unimplement sort');
  }
}

フィールド

フィールドについては大きく分けて4種類

def generate_const_field(field):
    modifier = field.extensions.printer.modifier()
    default_value = field.extensions.printer.assign(field.value)
    return f'static {modifier} {field.extensions.printer.get_type()} {field.name} = {default_value};'

def generate_non_reserved_field(self, field):
    const_field = self.get_paired_const_field(field)
    field_name = self.field_name(field)
    class_name = 'late ' + self.re_name(field.extensions.printer.get_type())
    body = f'{class_name} {field_name};\n'
    if const_field:
        return '@override\n' + body if self.is_transaction() and self.has_field_override(field_name) else body
    else:
        if self.is_nullable_field(field):
            return f'{class_name}? {field_name};\n'
        else:
            return '@override\n' + body if self.is_transaction() and self.has_field_override(field_name) else body

def generate_reserved_field(self, field):
    field_name = self.field_name(field)
    value = field.value
    return f'final {field.extensions.printer.get_type()} {field_name} = {value}; // reserved field\n'

def generate_type_hints(self):
    body = '\n'
    body += 'static const Map<String, String> TYPE_HINTS = {\n'
    hints = []
    for field in self.non_reserved_fields():
        if not field.extensions.printer.type_hint:
            continue

        hints.append(f'\'{field.extensions.printer.name}\': \'{field.extensions.printer.type_hint}\'')

    body += indent(',\n'.join(hints))
    body += '};\n'
    return body

これらをここで一気に出力してます。

def get_fields(self):
	return list(map(self.generate_const_field, self.const_fields())) + list(map(self.generate_non_reserved_field, self.non_reserved_fields())) + list(map(self.generate_reserved_field, self.reserved_fields())) + [self.generate_type_hints()]

ここは結構重要なところで、言語によって大きく変わると思います。最後のhintsはあまり気にしなくていいのですが他は完成を先に決めてから逆算してどう出力させたいかを調整します。

コンストラクタ

def get_ctor_descriptor(self):
    args = []
    body = ''
    for field in self.non_reserved_fields():
        arg_name = self.field_name(field, is_argument=True)
        args.append(f'{self.re_name(field.extensions.printer.get_type())}? {arg_name}') if not self.is_fields_one() else args.append(f'[{arg_name}]')

        const_field = self.get_paired_const_field(field)
        field_name = self.field_name(field)

        if const_field:
            body += f'this.{field_name} = {arg_name} ?? {self.typename}.{const_field.name};\n'
        else:
            # needs to be null or else field will not be destination when copying descriptor properties
            value = 'null' if self.is_nullable_field(field) else field.extensions.printer.get_default_value()
            body += f'this.{field_name} = {arg_name} ?? {value};\n'

    return MethodDescriptor(body=body, arguments=args)

pythonやjsではコンストラクタに引数を持っていませんが、dartでは引数に名前をつけることが出来たので工夫して、このようにしました。そのため、このように扱うことができます

var tx = TransferTransactionV1(
    network: NetworkType.TESTNET,
    deadline: Timestamp(facade.network.fromDatetime(DateTime.now().toUtc()).addHours(2).timestamp),
    signerPublicKey: PublicKey(keyPair.publicKey.bytes),
    recipientAddress: UnresolvedAddress('TA5LGYEWS6L2WYBQ75J2DGK7IOZHYVWFWRLOFWI'),
    message: utf8ToBytes('Hello, Symbol!!'),
    mosaics: [UnresolvedMosaic(mosaicId: UnresolvedMosaicId('56148181AF8A6CFC'), amount: Amount(1))],
  );

get_comparer_descriptor,generate_condition,get_sort_descriptorは割愛しますが、多少の修正は必要だと思います。

デシリアライズ

def generate_deserialize_field(self, field, arg_buffer_name=None):
    condition = self.generate_condition(field)

    buffer_name = arg_buffer_name or 'buffer'
    field_name = field.extensions.printer.name
    field_name_ = field_name if condition else 'var ' + field_name

    # half-hack: limit buffer to amount specified in size field
    buffer_load_name = buffer_name
    size_fields = field.extensions.size_fields
    if size_fields:
        assert len(size_fields) == 1, f'unexpected number of size_fields associated with {field.name}'
        buffer_load_name = 'buffer'

    adjust = ''
    field_size = field.extensions.printer.advancement_size()
    if field.display_type == DisplayType.TYPED_ARRAY:
        load = f'{field.extensions.printer.load(buffer_load_name)}'
        adjust += f'{buffer_name} = {buffer_name}.sublist({lang_field_name(field_size)});\n'
    else:
        field_size = field.extensions.printer.advancement_size()
        if field.display_type == DisplayType.INTEGER:
            buffer_load_name = 'buffer'
        elif field.display_type == DisplayType.BYTE_ARRAY:
            buffer_load_name = f'buffer.sublist(0, {lang_field_name(field.size)})'
            field_size = lang_field_name(field.size)
        load = field.extensions.printer.load(buffer_load_name)
        if self.struct.size == field.extensions.printer.name:
            adjust += f'{buffer_name} = {buffer_name}.sublist(0, size);\n'	
        adjust += f'{buffer_name} = {buffer_name}.sublist({field_size});\n'
    deserialize = f'{field_name_} = {load};'

    additional_statements = ''
    if is_reserved(field):
        assert_message = f'throw RangeError(\'Invalid value of reserved field (${field.extensions.printer.name})\');'
        additional_statements = f'if ({field.value} != {field.extensions.printer.name}) {{\n\t{assert_message}\n}}\n'

    if is_bound_size(field) and field.is_size_reference:
        additional_statements += '//marking sizeof field\n'

    deserialize_field = deserialize + '\n' + adjust + additional_statements

    value = '0' if field.extensions.printer.get_type() == 'Uint8List' else ''
    if condition:
        condition = f'var {field.extensions.printer.name} = null;\n' + condition

    return indent_if_conditional(condition, deserialize_field)

結構ややこしいところです。デシリアライズを順に進めますが書くフィールドのタイプによって制御を変えます。何度もビルドして修正すれば徐々に理解が進みます。
bufferを読めばポイントが先に進むライブラリがあればいいのですが無い場合は(多分dartは無理だった)バイナリを読み込んだらそのサイズ分進めてあげます。それがadjustのあたりです。

ここではタイプなどで制御を変えますが読み込み箇所はプリンターのload()に任せます。※プリンターは後ほど

シリアライズ

デシリアライズと反対です

def generate_serialize_field(self, field):
    condition = self.generate_condition(field, True)

    field_value = ''
    field_comment = ''

    # bound fields are the size / count / sizeof fields that are bound to either object or array
    bound_field = field.extensions.bound_field
    if is_bound_size(field):
        bound_field_name = self.field_name(bound_field)
        field_comment = f'  # {field.name}'

        if bound_field.display_type.is_array:
            if field.name.endswith('_count') or not bound_field.field_type.is_byte_constrained:
                field_value = f'{bound_field_name}.length'

                bound_condition = self.generate_condition(bound_field, True)
                if condition and bound_condition:
                    raise RuntimeError('do not know yet how to generate both conditions')

                # HACK: create inline if condition (for NEM namespace purposes)
                if bound_condition:
                    condition_value = bound_field.value.value
                    field_value = f'({bound_field_name} != null ? {field_value.replace(".", "!.")} : {condition_value})'
            else:
                field_value = bound_field.extensions.printer.get_size()
        elif field.is_size_reference:
            field_value = bound_field.extensions.printer.get_size()
    else:
        field_value = self.field_name(field)

    adjust = ''
    serialize_field = f'{field.extensions.printer.store(field_value, "currentPos")}'
    if field.display_type == DisplayType.TYPED_ARRAY:

        serialize_field = f'sort();\nvar res_{field.extensions.printer.name} = {serialize_field}'
        adjust = f'currentPos = res_{field_value}.item2;\n'
        adjust += f'buffer = res_{field_value}.item1;\n'
    else:
        size = field.extensions.printer.get_size()
        adjust = f'currentPos += {size};\n'
        if self.is_nullable_field(field):
            adjust = f'currentPos += {size.replace(".", "!.")};\n'
            serialize_field = f'{field.extensions.printer.store(field_value + "!", "currentPos")}'
        
    return indent_if_conditional(condition, f'{serialize_field};\n{adjust}')

同じく書き込み用のライブラリがポインタを先に進めなければadjustで調整する
書き込みはプリンターのstore()で。

シリアライズ、デシリアライズはそれぞれフィールド用のメソッドとガワを作るdescriptorがあります。

サイズ

同じくサイズにもそれぞれあります

def generate_size_field(self, field):
    condition = self.generate_condition(field, True)
    size_field = field.extensions.printer.get_size()
    if self.is_nullable_field(field):
        size_field = size_field.replace(".", "!.")

    return indent_if_conditional(condition, f'size += {size_field};\n')

def get_size_descriptor(self):
    body = 'var size = 0;\n'
    body += ''.join(map(self.generate_size_field, self.non_const_fields()))
    body += 'return size;'
    annotations = ''
    return MethodDescriptor(body=body, annotations=annotations)

ゲッター、セッター

dartの場合はフィールドでゲッターセッターをまとめました。言語によってはできない場合もあると思います。(dartも最初は両方用意してたけどスッキリするためにまとめた)

create_getter_descriptorでメソッドの構造を作ってget_getter_descriptorsで生成します。

def get_getter_descriptors(self):
	return list(map(self.create_getter_descriptor, self.computed_fields()))

mapの第一引数がディスクリプタで第二引数でゲッターを作成したいフィールドを渡します。上はNEM用でNEMだけcomputedというフィールドでゲッターを作成しました

プリンター

さて、プリンターに進みます。
Structタイプのシリアライズ、デシリアライズではその構造体が持つフィールドのシリアライズ、デシリアライズを行いますが、そのフィールドがIntやArrayタイプの場合はそちらのプリンターが仕事をしてくれます。

そうじゃない場合の残りは2つで、TypedArrayPrinterとBuiltinPrinterです。前者はmosaicsやtransactionsなどのようにそれ自体が配列の場合のプリンター、後者はStructタイプとEnumタイプのフィールド用のプリンターです

TypedArrayPrinter

コードを書くと長くなるのでそれはリポジトリでも見てもらうとして、ここではarrayHelpersで用意した関数の説明にしてみます。
デシリアライズは読み込み(read)、シリアライズは書き込み(write)です。

読み

  • readArrayImpl(readArray, readArrayCount)
  • readVariableSizeElements

書き

  • writeArrayImpl(writeArray, writeArrayCount)
  • writeVariableSizeElements

ざくっと2つずつあります。

前者はただの読み書きです。渡された配列分の読み書きを行います。dartの場合はポインタが進まないので戻り値で読み書きしたデータとサイズを返しました。

後者が少し特殊でalignmentという引数があります。これはサイズ調整で今のところ8しか見ていないです。例えばtransactionが2つの配列を渡したとして1つ目のサイズが220であれば8の倍数である224まで0埋めします。

BuiltinPrinter

こちらのほうがそんなに難しくなくて、loadもstoreもStructTypeのシリアライズとデシリアライズを使うので分岐はありません。
※この後作成するfactoryタイプでも使うのでそれだけ例外

FactoryFormatter

ここまで来ればおそらくやることはだいたい理解できていて、コードの解説はほぼ不要かと思います。
Factoryとは何かなどの話をしておきます。

まず、現時点ではTransactionFactory, EmbeddedTransactionFactory, BlockFactory, ReceiptFactoryの4つがmodelsに吐き出されます。これらはdesirializeとcreateByNameという2つの関数しか持ちません。まずcreateByNameについてはdartでは使いませんが、実装しました。

jsの場合にトランザクションを構築する方法としてfacadeからcreate関数で作成できます。typeを文字列で渡してるのでそれを元にどのトランザクションを作成するか判断する必要があります。これがcreateByNameの役割です。

const transaction = facade.transactionFactory.create({
    type: 'transfer_transaction_v1',
    signerPublicKey: aliceKeyPair.publicKey,
    recipientAddress: 'BOB_PLAIN_ADDRESS',
    mosaics: [
		{ 
            mosaicId: 0x72C0212E67A08BCEn,
            amount: 1_000000n 
        }
	],
    message: [0,...(new TextEncoder('utf-8')).encode('Hello, Symbol!!')],
    fee: 1_000000n,
    deadline,
});

今回のSDKではこのようなトランザクションの作成は解説しません。過去作成した時はこれがかなり大変でした。
一方dartでは

var tx = TransferTransactionV1(
    network: NetworkType.TESTNET,
    deadline: Timestamp(facade.network.fromDatetime(DateTime.now().toUtc()).addHours(2).timestamp),
    signerPublicKey: PublicKey(keyPair.publicKey.bytes),
    recipientAddress: UnresolvedAddress('TA5LGYEWS6L2WYBQ75J2DGK7IOZHYVWFWRLOFWI'),
    message: utf8ToBytes('Hello, Symbol!!'),
    mosaics: [UnresolvedMosaic(mosaicId: UnresolvedMosaicId('56148181AF8A6CFC'), amount: Amount(1))],
);

これで作成します。なのでtransactionFactory(今回作成するTransactionFactoryとは別クラスです)が不要でそのためcraeteByNameも不要です。

ちなみにjsでも

var tx = TransferTransactionV1();
tx.network = SymbolNetwork.NetworkType.TESTNET;

インスタンスを作成してから値を埋めていけば同じようなことはできます。

さて、もう一つの関数デシリアライズですが、これは他のクラスと同じでペイロードをトランザクションやブロック、レシートのオブジェクトにデシリアライズするための関数です。

受け取ったペイロードがどのトランザクションクラスか分からなくてもここに放り込めば、正しくオブジェクトにデシリアライズしてくれます。

BlockFactoryであればノード上のブロックのバイナリデータを元にブロック情報を取得することも可能です。

まず、受け取ったペイロードはTransactionならTransaction.desirialize()によってどのトランザクションか判別します。(TransactionTypeだけ分かればいい)
次にそのparentのtypeとversionを元に辞書型のオブジェクトからクラスのタイプを見分け、そのクラスのデシリアライズ関数でオブジェクトの作成です。

このとき

return factory_class.deserialize(buffer);

のようになるのでfactory_classはデシリアライズ関数を持っていなければなりません。jsやpythonでは型がゆるいのでそのまま使えちゃいますが(dartでもdynamicを使えばいいけれども)言語によっては厳しい物もあると思うのでインターフェースなどで対応が必要です。

テスト

ここまで出来たらビルドしてmodelsを作成しましょう。きっと意図していない構文やエラーに見舞われるはずです。まずはエラーが消えるよう潰していきます。

最後にエラーがなくなったらテストしましょう。

テスト用のファイルがここにあります

ローカルリポジトリにも同じ場所にあります。

otherはどうやらデータが不十分なので他の3つに対してテストします。

dartの例ですが

void main() async {
	group('transaction test', () => {  
    test('AccountAddressRestrictionTransactionV1_account_address_restriction_single_1', () {
      var payload = 'D0000000000000007695D855CBB6CB83D5BD08E9D76145674F805D741301883387B7101FD8CA84329BB14DBF2F0B4CD58AA84CF31AC6899D134FC38FAB0E7A76F6216ACD60914ACBD294E5E650ACC2A911B548BE2A1806FF4717621BCE3EC1007295219AFFC17B820000000001985041E0FEEEEFFEEEEFFEE0711EE7711EE77101000201000000009841E5B8E40781CF74DABF592817DE48711D778648DEAFB298F409274B52FABBFBCF7E7DF7E20DE1D0C3F657FB8595C1989059321905F681BCF47EA33BBF5E6F8298B5440854FDED';
      var tx = AccountAddressRestrictionTransactionV1(
        network: NetworkType.TESTNET,
        restrictionFlags: AccountRestrictionFlags.ADDRESS,
        restrictionAdditions: [
          UnresolvedAddress('TBA6LOHEA6A465G2X5MSQF66JBYR254GJDPK7MQ'),
          UnresolvedAddress('TD2ASJ2LKL5LX66PPZ67PYQN4HIMH5SX7OCZLQI')
        ],
        restrictionDeletions: [
          UnresolvedAddress('TCIFSMQZAX3IDPHUP2RTXP26N6BJRNKEBBKP33I')
        ],
        signerPublicKey: PublicKey('D294E5E650ACC2A911B548BE2A1806FF4717621BCE3EC1007295219AFFC17B82'),
        signature: Signature('7695D855CBB6CB83D5BD08E9D76145674F805D741301883387B7101FD8CA84329BB14DBF2F0B4CD58AA84CF31AC6899D134FC38FAB0E7A76F6216ACD60914ACB'),
        fee: Amount('18370164183782063840'),
        deadline: Timestamp(8207562320463688160)
      );
      expect(bytesToHex((TransactionFactory().deserialize(payload) as ITransaction).serialize()), payload);
      expect(bytesToHex(tx.serialize()).toUpperCase(), payload);
    }),

全てのトランザクション、ブロック、レシートに対してテストを行い、全てサクセスすれば晴れてmodelsの完成です。

ここまで来たら折り返し地点は過ぎています。最低限署名さえできればトランザクションはアナウンスできます。ただ、それだけでは少し物足りないので次回、署名やもろもろの話をしたいと思います。

8
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4