diff --git a/lib/src/soroban/soroban_contract_parser.dart b/lib/src/soroban/soroban_contract_parser.dart new file mode 100644 index 0000000..9d72704 --- /dev/null +++ b/lib/src/soroban/soroban_contract_parser.dart @@ -0,0 +1,220 @@ +// Copyright 2024 The Stellar Flutter SDK Authors. All rights reserved. +// Use of this source code is governed by a license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; + +import 'package:stellar_flutter_sdk/src/xdr/xdr_contract.dart'; +import 'package:stellar_flutter_sdk/src/xdr/xdr_data_io.dart'; + +/// Parses a soroban contract byte code to get Environment Meta, Contract Spec and Contract Meta. +/// see: https://developers.stellar.org/docs/tools/sdks/build-your-own +class SorobanContractParser { + /// Parses a soroban contract [byteCode] to get Environment Meta, Contract Spec and Contract Meta. + /// see: https://developers.stellar.org/docs/tools/sdks/build-your-own + /// Returns [SorobanContractInfo] containing the parsed data. + /// Throws [SorobanContractParserFailed] if any exception occurred during the byte code parsing. E.g. invalid byte code. + static SorobanContractInfo parseContractByteCode(Uint8List byteCode) { + String bytesString = new String.fromCharCodes(byteCode); + var xdrEnvMeta = _parseEnvironmentMeta(bytesString); + if (xdrEnvMeta == null || xdrEnvMeta.interfaceVersion == null) { + throw SorobanContractParserFailed( + 'invalid byte code: environment meta not found.'); + } + var specEntries = _parseContractSpec(bytesString); + if (specEntries == null) { + throw SorobanContractParserFailed( + 'invalid byte code: spec entries not found.'); + } + var metaEntries = _parseMeta(bytesString); + + return SorobanContractInfo( + xdrEnvMeta.interfaceVersion!.uint64, specEntries, metaEntries); + } + + static XdrSCEnvMetaEntry? _parseEnvironmentMeta(String bytesString) { + try { + var metaEnvEntryBytes = _extractStringBetween( + bytesString, 'contractenvmetav0', 'contractmetav0'); + if (metaEnvEntryBytes == null) { + metaEnvEntryBytes = _extractStringBetween( + bytesString, 'contractenvmetav0', 'contractspecv0'); + } + if (metaEnvEntryBytes == null) { + metaEnvEntryBytes = + _extractStringToEnd(bytesString, 'contractenvmetav0'); + } + + if (metaEnvEntryBytes == null) { + return null; + } + + var bytes = new Uint8List.fromList(metaEnvEntryBytes.codeUnits); + return XdrSCEnvMetaEntry.decode(XdrDataInputStream(bytes)); + } on Exception catch (_) { + return null; + } on Error catch (_) { + return null; + } + } + + static List? _parseContractSpec(String bytesString) { + var specBytesString = _extractStringBetween( + bytesString, 'contractspecv0', 'contractenvmetav0'); + if (specBytesString == null) { + specBytesString = _extractStringBetween( + bytesString, 'contractspecv0', 'contractspecv0'); + } + if (specBytesString == null) { + specBytesString = _extractStringToEnd(bytesString, 'contractspecv0'); + } + + if (specBytesString == null) { + return null; + } + List result = List.empty(growable: true); + + while (specBytesString!.length > 0) { + try { + var bytes = new Uint8List.fromList(specBytesString.codeUnits); + var entry = XdrSCSpecEntry.decode(XdrDataInputStream(bytes)); + if (entry.discriminant == + XdrSCSpecEntryKind.SC_SPEC_ENTRY_FUNCTION_V0 || + entry.discriminant == + XdrSCSpecEntryKind.SC_SPEC_ENTRY_UDT_STRUCT_V0 || + entry.discriminant == + XdrSCSpecEntryKind.SC_SPEC_ENTRY_UDT_UNION_V0 || + entry.discriminant == + XdrSCSpecEntryKind.SC_SPEC_ENTRY_UDT_ENUM_V0 || + entry.discriminant == + XdrSCSpecEntryKind.SC_SPEC_ENTRY_UDT_ERROR_ENUM_V0) { + result.add(entry); + XdrDataOutputStream xdrOutputStream = XdrDataOutputStream(); + XdrSCSpecEntry.encode(xdrOutputStream, entry); + var entryBytes = xdrOutputStream.bytes; + var entryBytesString = new String.fromCharCodes(entryBytes); + var specBytesStrOrNull = + _extractStringToEnd(specBytesString, entryBytesString); + if (specBytesStrOrNull == null) { + break; + } + specBytesString = specBytesStrOrNull; + } else { + break; + } + } on Exception catch (_) { + break; + } on Error catch (_) { + break; + } + } + return result; + } + + static Map _parseMeta(String bytesString) { + var metaBytesString = _extractStringBetween( + bytesString, 'contractmetav0', 'contractenvmetav0'); + if (metaBytesString == null) { + metaBytesString = _extractStringBetween( + bytesString, 'contractmetav0', 'contractspecv0'); + } + if (metaBytesString == null) { + metaBytesString = _extractStringToEnd(bytesString, 'contractmetav0'); + } + + Map result = Map(); + + if (metaBytesString == null) { + return result; + } + + while (metaBytesString!.length > 0) { + try { + var bytes = new Uint8List.fromList(metaBytesString.codeUnits); + var entry = XdrSCMetaEntry.decode(XdrDataInputStream(bytes)); + if (entry.discriminant == XdrSCMetaKind.SC_META_V0) { + result[entry.v0!.key] = entry.v0!.value; + XdrDataOutputStream xdrOutputStream = XdrDataOutputStream(); + XdrSCMetaEntry.encode(xdrOutputStream, entry); + var entryBytes = xdrOutputStream.bytes; + var entryBytesString = new String.fromCharCodes(entryBytes); + var metaBytesStrOrNull = + _extractStringToEnd(metaBytesString, entryBytesString); + if (metaBytesStrOrNull == null) { + break; + } + metaBytesString = metaBytesStrOrNull; + } else { + break; + } + } on Exception catch (_) { + break; + } on Error catch (_) { + break; + } + } + return result; + } + + static String? _extractStringBetween( + String input, String startSymbol, String endSymbol) { + int startIndex = input.indexOf(startSymbol); + int endIndex = input.indexOf(endSymbol, startIndex + startSymbol.length); + + if (startIndex != -1 && endIndex != -1) { + return input.substring(startIndex + startSymbol.length, endIndex); + } + + return null; + } + + static String? _extractStringToEnd(String input, String startSymbol) { + int startIndex = input.indexOf(startSymbol); + + if (startIndex != -1) { + return input.substring(startIndex + startSymbol.length); + } + + return null; + } +} + +/// Thrown if the [SorobanContractParser] failed parsing the given byte code. +class SorobanContractParserFailed implements Exception { + String _message; + + SorobanContractParserFailed(this._message); + + String toString() { + return _message; + } +} + +/// Stores information parsed from a soroban contract byte code such as +/// Environment Meta, Contract Spec Entries and Contract Meta Entries. +/// See also: https://developers.stellar.org/docs/tools/sdks/build-your-own +class SorobanContractInfo { + /** + * Environment interface number from Environment Meta. + */ + int envInterfaceVersion; + + /** + * Contract Spec Entries. There is a SCSpecEntry for every function, struct, + * and union exported by the contract. + */ + List specEntries; + + /** + * Contract Meta Entries. Key => Value pairs. + * Contracts may store any metadata in the entries that can be used by applications + * and tooling off-network. + */ + Map metaEntries; + + /** + * Constructor. + */ + SorobanContractInfo( + this.envInterfaceVersion, this.specEntries, this.metaEntries); +} diff --git a/lib/src/soroban/soroban_server.dart b/lib/src/soroban/soroban_server.dart index 9a26b03..dfc612a 100644 --- a/lib/src/soroban/soroban_server.dart +++ b/lib/src/soroban/soroban_server.dart @@ -10,6 +10,7 @@ import 'package:dio/io.dart'; import 'package:stellar_flutter_sdk/src/account.dart'; import 'package:stellar_flutter_sdk/src/key_pair.dart'; import 'package:stellar_flutter_sdk/src/responses/response.dart'; +import 'package:stellar_flutter_sdk/src/soroban/soroban_contract_parser.dart'; import 'package:stellar_flutter_sdk/src/xdr/xdr_account.dart'; import 'package:stellar_flutter_sdk/src/xdr/xdr_type.dart'; import 'soroban_auth.dart'; @@ -215,6 +216,33 @@ class SorobanServer { return null; } + /// Loads contract source byte code for the given [contractId] and extracts + /// the information (Environment Meta, Contract Spec, Contract Meta). + /// Returns [SorobanContractInfo] or null if the contract was not found. + /// Throws [SorobanContractParserFailed] if parsing of the byte code failed. + Future loadContractInfoForContractId( + String contractId) async { + var contractCodeEntry = await loadContractCodeForContractId(contractId); + if (contractCodeEntry == null) { + return null; + } + var byteCode = contractCodeEntry.code.dataValue; + return SorobanContractParser.parseContractByteCode(byteCode); + } + + /// Loads contract source byte code for the given [wasmId] and extracts + /// the information (Environment Meta, Contract Spec, Contract Meta). + /// Returns [SorobanContractInfo] null if the contract was not found. + /// Throws [SorobanContractParserFailed] if parsing of the byte code failed. + Future loadContractInfoForWasmId(String wasmId) async { + var contractCodeEntry = await loadContractCodeForWasmId(wasmId); + if (contractCodeEntry == null) { + return null; + } + var byteCode = contractCodeEntry.code.dataValue; + return SorobanContractParser.parseContractByteCode(byteCode); + } + /// General info about the currently configured network. /// See: https://developers.stellar.org/network/soroban-rpc/api-reference/methods/getNetwork Future getNetwork() async { diff --git a/lib/src/xdr/xdr_contract.dart b/lib/src/xdr/xdr_contract.dart index de6545d..254ee0b 100644 --- a/lib/src/xdr/xdr_contract.dart +++ b/lib/src/xdr/xdr_contract.dart @@ -1216,6 +1216,83 @@ class XdrSCEnvMetaEntry { } } +class XdrSCMetaV0 { + String _key; + String get key => this._key; + set key(String value) => this._key = value; + + String _value; + String get value => this._value; + set value(String value) => this._value = value; + + XdrSCMetaV0(this._key, this._value); + + static void encode(XdrDataOutputStream stream, XdrSCMetaV0 encoded) { + stream.writeString(encoded.key); + stream.writeString(encoded.value); + } + + static XdrSCMetaV0 decode(XdrDataInputStream stream) { + String key = stream.readString(); + String value = stream.readString(); + return XdrSCMetaV0(key, value); + } +} + +class XdrSCMetaKind { + final _value; + const XdrSCMetaKind._internal(this._value); + toString() => 'SCMetaKind.$_value'; + XdrSCMetaKind(this._value); + get value => this._value; + + static const SC_META_V0 = const XdrSCMetaKind._internal(0); + + static XdrSCMetaKind decode(XdrDataInputStream stream) { + int value = stream.readInt(); + switch (value) { + case 0: + return SC_META_V0; + default: + throw Exception("Unknown enum value: $value"); + } + } + + static void encode(XdrDataOutputStream stream, XdrSCMetaKind value) { + stream.writeInt(value.value); + } +} + +class XdrSCMetaEntry { + XdrSCMetaEntry(this._kind); + XdrSCMetaKind _kind; + XdrSCMetaKind get discriminant => this._kind; + set discriminant(XdrSCMetaKind value) => this._kind = value; + + XdrSCMetaV0? _v0; + XdrSCMetaV0? get v0 => this._v0; + set v0(XdrSCMetaV0? value) => this._v0 = value; + + static void encode(XdrDataOutputStream stream, XdrSCMetaEntry encoded) { + stream.writeInt(encoded.discriminant.value); + switch (encoded.discriminant) { + case XdrSCMetaKind.SC_META_V0: + XdrSCMetaV0.encode(stream, encoded.v0!); + break; + } + } + + static XdrSCMetaEntry decode(XdrDataInputStream stream) { + XdrSCMetaEntry decoded = XdrSCMetaEntry(XdrSCMetaKind.decode(stream)); + switch (decoded.discriminant) { + case XdrSCMetaKind.SC_META_V0: + decoded.v0 = XdrSCMetaV0.decode(stream); + break; + } + return decoded; + } +} + class XdrSCSpecTypeOption { XdrSCSpecTypeDef _valueType; XdrSCSpecTypeDef get valueType => this._valueType; @@ -1688,9 +1765,9 @@ class XdrSCSpecUDTUnionCaseTupleV0 { String get name => this._name; set name(String value) => this._name = value; - XdrSCSpecTypeDef? _type; - XdrSCSpecTypeDef? get type => this._type; - set type(XdrSCSpecTypeDef? value) => this._type = value; + List _type; + List get type => this._type; + set type(List value) => this._type = value; XdrSCSpecUDTUnionCaseTupleV0(this._doc, this._name, this._type); @@ -1698,23 +1775,23 @@ class XdrSCSpecUDTUnionCaseTupleV0 { XdrDataOutputStream stream, XdrSCSpecUDTUnionCaseTupleV0 encoded) { stream.writeString(encoded.doc); stream.writeString(encoded.name); - if (encoded.type != null) { - stream.writeInt(1); - XdrSCSpecTypeDef.encode(stream, encoded.type!); - } else { - stream.writeInt(0); + int typeSize = encoded.type.length; + stream.writeInt(typeSize); + for (int i = 0; i < typeSize; i++) { + XdrSCSpecTypeDef.encode(stream, encoded.type[i]); } } static XdrSCSpecUDTUnionCaseTupleV0 decode(XdrDataInputStream stream) { String doc = stream.readString(); String name = stream.readString(); - XdrSCSpecTypeDef? typ; - int typePresent = stream.readInt(); - if (typePresent != 0) { - typ = XdrSCSpecTypeDef.decode(stream); + int typeSize = stream.readInt(); + List type = + List.empty(growable: true); + for (int i = 0; i < typeSize; i++) { + type.add(XdrSCSpecTypeDef.decode(stream)); } - return XdrSCSpecUDTUnionCaseTupleV0(doc, name, typ); + return XdrSCSpecUDTUnionCaseTupleV0(doc, name, type); } } diff --git a/test/soroban_test.dart b/test/soroban_test.dart index 4ce3f66..9d14300 100644 --- a/test/soroban_test.dart +++ b/test/soroban_test.dart @@ -420,6 +420,11 @@ void main() { var effectsPage = await sdk.effects.forAccount(accountAId).execute(); assert(effectsPage.records.isNotEmpty); + + var contractInfo = await sorobanServer.loadContractInfoForWasmId(helloContractWasmId!); + assert(contractInfo != null); + assert(contractInfo!.specEntries.length > 0); + assert(contractInfo!.metaEntries.length > 0); }); test('test create contract', () async { @@ -512,6 +517,11 @@ void main() { var effectsPage = await sdk.effects.forAccount(accountAId).execute(); assert(effectsPage.records.isNotEmpty); + + var contractInfo = await sorobanServer.loadContractInfoForContractId(helloContractId!); + assert(contractInfo != null); + assert(contractInfo!.specEntries.length > 0); + assert(contractInfo!.metaEntries.length > 0); }); test('test invoke contract', () async { @@ -767,6 +777,11 @@ void main() { assert(eventsResponse.events!.length > 0); await extendContractCodeFootprintTTL(eventsContractWasmId, 100000); + + var contractInfo = await sorobanServer.loadContractInfoForContractId(eventsContractId); + assert(contractInfo != null); + assert(contractInfo!.specEntries.length > 0); + assert(contractInfo!.metaEntries.length > 0); }); test('test get ledger entries', () async { diff --git a/test/soroban_test_atomic_swap.dart b/test/soroban_test_atomic_swap.dart index 0f2a330..0e5460b 100644 --- a/test/soroban_test_atomic_swap.dart +++ b/test/soroban_test_atomic_swap.dart @@ -530,8 +530,16 @@ void main() { tokenBContractWasmId = await installContract(tokenContractPath); await Future.delayed(Duration(seconds: 5)); await extendContractCodeFootprintTTL(tokenBContractWasmId!, 100000); + var contractInfo = await sorobanServer.loadContractInfoForWasmId(tokenBContractWasmId!); + assert(contractInfo != null); + assert(contractInfo!.specEntries.length > 0); + assert(contractInfo!.metaEntries.length > 0); swapContractWasmId = await installContract(swapContractPath); await extendContractCodeFootprintTTL(swapContractWasmId!, 100000); + contractInfo = await sorobanServer.loadContractInfoForWasmId(swapContractWasmId!); + assert(contractInfo != null); + assert(contractInfo!.specEntries.length > 0); + assert(contractInfo!.metaEntries.length > 0); await Future.delayed(Duration(seconds: 5)); }); @@ -541,9 +549,17 @@ void main() { await Future.delayed(Duration(seconds: 5)); tokenBContractId = await createContract(tokenBContractWasmId!); print("Token B Contract ID: " + tokenBContractId!); + var contractInfo = await sorobanServer.loadContractInfoForContractId(tokenBContractId!); + assert(contractInfo != null); + assert(contractInfo!.specEntries.length > 0); + assert(contractInfo!.metaEntries.length > 0); await Future.delayed(Duration(seconds: 5)); swapContractId = await createContract(swapContractWasmId!); print("SWAP Contract ID: " + swapContractId!); + contractInfo = await sorobanServer.loadContractInfoForContractId(swapContractId!); + assert(contractInfo != null); + assert(contractInfo!.specEntries.length > 0); + assert(contractInfo!.metaEntries.length > 0); await Future.delayed(Duration(seconds: 5)); }); diff --git a/test/soroban_test_parser.dart b/test/soroban_test_parser.dart new file mode 100644 index 0000000..998192f --- /dev/null +++ b/test/soroban_test_parser.dart @@ -0,0 +1,240 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:stellar_flutter_sdk/src/soroban/soroban_contract_parser.dart'; +import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart'; + +void main() { + + String contractPath = + "/Users/chris/Soneso/github/stellar_flutter_sdk/test/wasm/soroban_token_contract.wasm"; + + String _getSpecTypeInfo(XdrSCSpecTypeDef specType) { + switch(specType.discriminant) { + case XdrSCSpecType.SC_SPEC_TYPE_VAL: + return "val"; + case XdrSCSpecType.SC_SPEC_TYPE_BOOL: + return "bool"; + case XdrSCSpecType.SC_SPEC_TYPE_VOID: + return "void"; + case XdrSCSpecType.SC_SPEC_TYPE_ERROR: + return "error"; + case XdrSCSpecType.SC_SPEC_TYPE_U32: + return "u32"; + case XdrSCSpecType.SC_SPEC_TYPE_I32: + return "i32"; + case XdrSCSpecType.SC_SPEC_TYPE_U64: + return "u64"; + case XdrSCSpecType.SC_SPEC_TYPE_I64: + return "i64"; + case XdrSCSpecType.SC_SPEC_TYPE_TIMEPOINT: + return "timepoint"; + case XdrSCSpecType.SC_SPEC_TYPE_DURATION: + return "duration"; + case XdrSCSpecType.SC_SPEC_TYPE_U128: + return "u128"; + case XdrSCSpecType.SC_SPEC_TYPE_I128: + return "i128"; + case XdrSCSpecType.SC_SPEC_TYPE_U256: + return "u256"; + case XdrSCSpecType.SC_SPEC_TYPE_I256: + return "i256"; + case XdrSCSpecType.SC_SPEC_TYPE_BYTES: + return "bytes"; + case XdrSCSpecType.SC_SPEC_TYPE_STRING: + return "string"; + case XdrSCSpecType.SC_SPEC_TYPE_SYMBOL: + return "symbol"; + case XdrSCSpecType.SC_SPEC_TYPE_ADDRESS: + return "address"; + case XdrSCSpecType.SC_SPEC_TYPE_OPTION: + var valueType = _getSpecTypeInfo(specType.option!.valueType); + return "option (value type: $valueType)"; + case XdrSCSpecType.SC_SPEC_TYPE_RESULT: + var okType = _getSpecTypeInfo(specType.result!.okType); + var errorType = _getSpecTypeInfo(specType.result!.errorType); + return "result (ok type: $okType , error type: $errorType)"; + case XdrSCSpecType.SC_SPEC_TYPE_VEC: + var elementType = _getSpecTypeInfo(specType.vec!.elementType); + return "vec (element type: $elementType)"; + case XdrSCSpecType.SC_SPEC_TYPE_MAP: + var keyType = _getSpecTypeInfo(specType.map!.keyType); + var valueType = _getSpecTypeInfo(specType.map!.valueType); + return "map (key type: $keyType , value type: $valueType)"; + case XdrSCSpecType.SC_SPEC_TYPE_TUPLE: + var valueTypesStr = "["; + for (var valueType in specType.tuple!.valueTypes) { + valueTypesStr += "${_getSpecTypeInfo(valueType)},"; + } + valueTypesStr += "]"; + return "tuple (value types: $valueTypesStr)"; + case XdrSCSpecType.SC_SPEC_TYPE_BYTES_N: + return "bytesN (n: ${specType.bytesN!.n.uint32})"; + case XdrSCSpecType.SC_SPEC_TYPE_UDT: + return "udt (name: ${specType.udt!.name})"; + default: + return "unknown"; + } + } + + _printFunction(XdrSCSpecFunctionV0 function) { + print("Function: ${function.name}"); + var index = 0; + for (var input in function.inputs) { + print("input[$index] name: ${input.name}"); + print("input[$index] type: ${_getSpecTypeInfo(input.type)}"); + if (input.doc.length > 0) { + print("input[$index] doc: ${input.doc}"); + } + index ++; + } + index = 0; + for (var output in function.outputs) { + print("output[$index] type: ${_getSpecTypeInfo(output)}"); + index ++; + } + if (function.doc.length > 0) { + print("doc : ${function.doc}"); + } + } + + _printUdtStruct(XdrSCSpecUDTStructV0 udtStruct) { + print("UDT Struct: ${udtStruct.name}"); + if (udtStruct.lib.length > 0) { + print("lib : ${udtStruct.lib}"); + } + var index = 0; + for (var field in udtStruct.fields) { + print("field[$index] name: ${field.name}"); + print("field[$index] type: ${_getSpecTypeInfo(field.type)}"); + if (field.doc.length > 0) { + print("field[$index] doc: ${field.doc}"); + } + index ++; + } + if (udtStruct.doc.length > 0) { + print("doc : ${udtStruct.doc})"); + } + } + + _printUdtUnion(XdrSCSpecUDTUnionV0 udtUnion) { + print("UDT Union: ${udtUnion.name}"); + if (udtUnion.lib.length > 0) { + print("lib : ${udtUnion.lib}"); + } + var index = 0; + for (var uCase in udtUnion.cases) { + switch(uCase.discriminant) { + case XdrSCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_VOID_V0: + print("case[$index] is voidV0"); + print("case[$index] name: ${uCase.voidCase!.name}"); + if (uCase.voidCase!.doc.length > 0) { + print("case[$index] doc: ${uCase.voidCase!.doc}"); + } + break; + case XdrSCSpecUDTUnionCaseV0Kind.SC_SPEC_UDT_UNION_CASE_TUPLE_V0: + print("case[$index] is tupleV0"); + print("case[$index] name: ${uCase.tupleCase!.name}"); + var valueTypesStr = "["; + for (var valueType in uCase.tupleCase!.type) { + valueTypesStr += "${_getSpecTypeInfo(valueType)},"; + } + valueTypesStr += "]"; + print("case[$index] types: $valueTypesStr"); + if (uCase.tupleCase!.doc.length > 0) { + print("case[$index] doc: ${uCase.tupleCase!.doc}"); + } + break; + } + index ++; + } + if (udtUnion.doc.length > 0) { + print("doc : ${udtUnion.doc})"); + } + } + + _printUdtEnum(XdrSCSpecUDTEnumV0 udtEnum) { + print("UDT Enum : ${udtEnum.name}"); + if (udtEnum.lib.length > 0) { + print("lib : ${udtEnum.lib}"); + } + var index = 0; + for (var uCase in udtEnum.cases) { + print("case[$index] name: ${uCase.name}"); + print("case[$index] value: ${uCase.value}"); + if (uCase.doc.length > 0) { + print("case[$index] doc: ${uCase.doc}"); + } + index ++; + } + if (udtEnum.doc.length > 0) { + print("doc : ${udtEnum.doc}"); + } + } + + _printUdtErrorEnum(XdrSCSpecUDTErrorEnumV0 udtErrorEnum) { + print("UDT Error Enum : ${udtErrorEnum.name}"); + if (udtErrorEnum.lib.length > 0) { + print("lib : ${udtErrorEnum.lib}"); + } + var index = 0; + for (var uCase in udtErrorEnum.cases) { + print("case[$index] name: ${uCase.name}"); + print("case[$index] value: ${uCase.value}"); + if (uCase.doc.length > 0) { + print("case[$index] doc: ${uCase.doc}"); + } + index ++; + } + if (udtErrorEnum.doc.length > 0) { + print("doc : ${udtErrorEnum.doc}"); + } + } + + test('test token contract parsing', () async { + + var byteCode = await Util.readFile(contractPath); + var contractInfo = SorobanContractParser.parseContractByteCode(byteCode); + assert(contractInfo.specEntries.length == 17); + assert(contractInfo.metaEntries.length == 2); + print("--------------------------------"); + print("Env Meta:"); + print(""); + print("Interface version: ${contractInfo.envInterfaceVersion}"); + print("--------------------------------"); + print("Contract Meta:"); + print(""); + for (var metaEntry in contractInfo.metaEntries.entries) { + print("${metaEntry.key}: ${metaEntry.value}"); + } + print("--------------------------------"); + + print("Contract Spec:"); + print(""); + var specEntries = contractInfo.specEntries; + var index = 0; + for (var specEntry in specEntries) { + switch (specEntry.discriminant) { + case XdrSCSpecEntryKind.SC_SPEC_ENTRY_FUNCTION_V0: + _printFunction(specEntry.functionV0!); + break; + case XdrSCSpecEntryKind.SC_SPEC_ENTRY_UDT_STRUCT_V0: + _printUdtStruct(specEntry.udtStructV0!); + break; + case XdrSCSpecEntryKind.SC_SPEC_ENTRY_UDT_UNION_V0: + _printUdtUnion(specEntry.udtUnionV0!); + break; + case XdrSCSpecEntryKind.SC_SPEC_ENTRY_UDT_ENUM_V0: + _printUdtEnum(specEntry.udtEnumV0!); + break; + case XdrSCSpecEntryKind.SC_SPEC_ENTRY_UDT_ERROR_ENUM_V0: + _printUdtErrorEnum(specEntry.udtErrorEnumV0!); + break; + default: + print('specEntry [$index] -> kind(${specEntry.discriminant.value}): unknown'); + } + print(""); + index++; + } + print("--------------------------------"); + }); + +} \ No newline at end of file