この記事の続き
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を継承するようにしました。
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');
}
}
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の完成です。
ここまで来たら折り返し地点は過ぎています。最低限署名さえできればトランザクションはアナウンスできます。ただ、それだけでは少し物足りないので次回、署名やもろもろの話をしたいと思います。