diff --git a/lib/codec_utils.dart b/lib/codec_utils.dart index f72f7e5..0dc00c1 100644 --- a/lib/codec_utils.dart +++ b/lib/codec_utils.dart @@ -11,7 +11,7 @@ export 'src/codecs/base/base58_codec.dart'; /// Classes designed for encoding data using the Bech32 encoding scheme. /// Usage: -/// `` +/// ``` /// List convertedUint5List = BytesUtils.convertBits(Bech32.uint8List, 8, 5, padBool: true); /// /// Bech32 bech32 = Bech32.fromUint5List('bc', convertedUint5List) @@ -26,9 +26,28 @@ export 'src/codecs/base/base58_codec.dart'; /// ``` export 'src/codecs/bech32/export.dart'; +/// The [ByteReader] class is designed for sequential reading of binary data and tracking the current [offset] of bytes. +/// Usage: +/// ``` +/// ByteReader byteReader = ByteReader(Uint8List.fromList([0x01, 0x02, 0x03, 0x04])); +/// int byte = reader.shiftRight(); +/// Uint8List bytes = reader.shiftRightBy(2); +/// byteReader.shiftLeftBy(3); +/// ``` +export 'src/codecs/byte_reader/byte_reader.dart'; + /// Defines available CBOR data structures export 'src/codecs/cbor/export.dart'; +/// The [CompactU16Decoder] class is designed for decoding the first 16-bit unsigned integer encoded in a compact, variable-length format +/// from an object of the [ByteReader] class at its current [offset]. +/// Usage: +/// ``` +/// ByteReader byteReader = ByteReader(Uint8List.fromList([0xFF, 0xFF, 0x03])); +/// int decodedValue = CompactU16Decoder.decode(byteReader); +/// ``` +export 'src/codecs/compact_u16/compact_u16_decoder.dart'; + /// The [HexCodec] class is designed for encoding and decoding data using the hexadecimal encoding scheme. /// Usage: /// ``` @@ -44,79 +63,79 @@ export 'src/codecs/hex/hex_codec.dart'; /// ``` export 'src/codecs/protobuf/export.dart'; -/// Provides static utility methods for encoding and decoding data using the Recursive Length Prefix (RLP) encoding scheme. -/// Usage: -/// ``` -/// Uint8List encodedRlp = RLP.encode(RLPBytes()); -/// IRLPElement decodedRlp = RLP.decode(encodedRlp); -/// ``` +/// Provides static utility methods for encoding and decoding data using the Recursive Length Prefix (RLP) encoding scheme. +/// Usage: +/// ``` +/// Uint8List encodedRlp = RLP.encode(RLPBytes()); +/// IRLPElement decodedRlp = RLP.decode(encodedRlp); +/// ``` export 'src/codecs/rlp/rlp_codec.dart'; /// Defines Uniform Resource (UR) object, containing CBOR encoded data from QR code. /// Usage: -/// ``` -/// // Returns UR object with given type and CBOR encoded data -/// UR ur = UR(type: 'crypto-seed', cborPayload: cborData); +/// ``` +/// // Returns UR object with given type and CBOR encoded data +/// UR ur = UR(type: 'crypto-seed', cborPayload: cborData); /// -/// // Returns UR object from given [IURRegistryRecord] -/// Ur ur = UR.fromCborTaggedObject(cborTaggedObject); +/// // Returns UR object from given [IURRegistryRecord] +/// Ur ur = UR.fromCborTaggedObject(cborTaggedObject); /// -/// // Returns empty CBOR value -/// Ur ur = UR.empty(); +/// // Returns empty CBOR value +/// Ur ur = UR.empty(); /// -/// // Decodes CBOR payload of UR into CBOR value -/// CborValue cborValue = ur.decodeCborPayload(); -/// ``` +/// // Decodes CBOR payload of UR into CBOR value +/// CborValue cborValue = ur.decodeCborPayload(); +/// ``` export 'src/codecs/uniform_resource/ur.dart'; /// Provides functionality to decode data from Uniform Resource (UR) format from single or multi UR resource /// Usage: -/// ``` -/// // Construct URDecoder -/// URDecoder urDecoder = URDecoder(); +/// ``` +/// // Construct URDecoder +/// URDecoder urDecoder = URDecoder(); /// -/// // After reading UR data from QR code, pass it to URDecoder -/// urDecoder.receivePart(urPart); +/// // After reading UR data from QR code, pass it to URDecoder +/// urDecoder.receivePart(urPart); /// -/// // Check if URDecoder received whole data -/// bool receivedWholeDataBool = urDecoder.isComplete; +/// // Check if URDecoder received whole data +/// bool receivedWholeDataBool = urDecoder.isComplete; /// -/// // Return received data as [ICborTaggedObject] if possible -/// ICborTaggedObject? cborTaggedObject = urDecoder.buildCborTaggedObject(); +/// // Return received data as [ICborTaggedObject] if possible +/// ICborTaggedObject? cborTaggedObject = urDecoder.buildCborTaggedObject(); /// -/// // Return received data as [UR] if possible -/// UR? ur = urDecoder.buildUR(); +/// // Return received data as [UR] if possible +/// UR? ur = urDecoder.buildUR(); /// -/// // Return received parts percentage -/// double progress = urDecoder.progress; +/// // Return received parts percentage +/// double progress = urDecoder.progress; /// -/// // Return estimated percentage of received data -/// double estimatedPercentComplete = urDecoder.estimatedPercentComplete; +/// // Return estimated percentage of received data +/// double estimatedPercentComplete = urDecoder.estimatedPercentComplete; /// -/// // Return total parts count expected in current transfer -/// int expectedPartCount = urDecoder.expectedPartCount; -/// ``` +/// // Return total parts count expected in current transfer +/// int expectedPartCount = urDecoder.expectedPartCount; +/// ``` export 'src/codecs/uniform_resource/ur_decoder.dart'; /// Provides functionality to encode data into Uniform Resource (UR) format /// Usage: -/// ``` -/// // Construct UREncoder -/// UREncoder urEncoder = UREncoder(ur: ur); +/// ``` +/// // Construct UREncoder +/// UREncoder urEncoder = UREncoder(ur: ur); /// -/// // Encode whole data to transfer into UR format -/// List parts = urEncoder.encodeWhole(); +/// // Encode whole data to transfer into UR format +/// List parts = urEncoder.encodeWhole(); /// -/// // Return total parts count expected in current transfer -/// int fragmentsCount = urEncoder.fragmentsCount; +/// // Return total parts count expected in current transfer +/// int fragmentsCount = urEncoder.fragmentsCount; /// -/// // Encode next part of data to transfer into UR format -/// String part = urEncoder.nextPart(); +/// // Encode next part of data to transfer into UR format +/// String part = urEncoder.nextPart(); /// -/// // Return whether all parts were encoded -/// bool transferCompletedBool = urEncoder.isComplete; +/// // Return whether all parts were encoded +/// bool transferCompletedBool = urEncoder.isComplete; /// -/// // Reset UREncoder to start encoding from beginning -/// urEncoder.reset(); -/// ``` +/// // Reset UREncoder to start encoding from beginning +/// urEncoder.reset(); +/// ``` export 'src/codecs/uniform_resource/ur_encoder.dart'; diff --git a/lib/src/codecs/base/base58_codec.dart b/lib/src/codecs/base/base58_codec.dart index f573604..d1f6b0f 100644 --- a/lib/src/codecs/base/base58_codec.dart +++ b/lib/src/codecs/base/base58_codec.dart @@ -94,7 +94,10 @@ class Base58Codec { } } - return Uint8List.fromList([...List.filled(padLen, 0), ...bytes]); + return Uint8List.fromList([ + ...List.filled(padLen, 0), + if ((bytes[0] == 0 && bytes.length == 1) == false) ...bytes, + ]); } static List _computeChecksum(Uint8List dataBytes) { diff --git a/lib/src/codecs/byte_reader/byte_reader.dart b/lib/src/codecs/byte_reader/byte_reader.dart new file mode 100644 index 0000000..62ed811 --- /dev/null +++ b/lib/src/codecs/byte_reader/byte_reader.dart @@ -0,0 +1,32 @@ +import 'dart:typed_data'; + +/// A helper class used for sequential reading of bytes from a [Uint8List] with [_offset] tracking. +class ByteReader { + final Uint8List data; + int _offset = 0; + + ByteReader(this.data); + + int get offset => _offset; + + /// Moves the [_offset] backward by [count] bytes. + void shiftLeftBy(int count) { + if (_offset < count) { + throw Exception('Offset out of bounds'); + } + _offset -= count; + } + + /// Reads 1 byte the at current [_offset] and moves the [_offset] forward by 1 byte. + int shiftRight() => shiftRightBy(1)[0]; + + /// Reads [count] bytes starting at the current [_offset] and moves the [_offset] forward by [count] bytes. + Uint8List shiftRightBy(int count) { + if (_offset + count > data.length) { + throw Exception('Offset out of bounds'); + } + Uint8List bytes = data.sublist(_offset, _offset + count); + _offset += count; + return bytes; + } +} diff --git a/lib/src/codecs/cbor/a_cbor_tagged_object.dart b/lib/src/codecs/cbor/a_cbor_tagged_object.dart index f282f1f..335ffdc 100644 --- a/lib/src/codecs/cbor/a_cbor_tagged_object.dart +++ b/lib/src/codecs/cbor/a_cbor_tagged_object.dart @@ -7,6 +7,8 @@ import 'package:codec_utils/src/codecs/cbor/crypto/cbor_crypto_hd_key.dart'; import 'package:codec_utils/src/codecs/cbor/crypto/cbor_crypto_keypath.dart'; import 'package:codec_utils/src/codecs/cbor/ethereum/cbor_eth_sign_request.dart'; import 'package:codec_utils/src/codecs/cbor/ethereum/cbor_eth_signature.dart'; +import 'package:codec_utils/src/codecs/cbor/solana/cbor_sol_sign_request.dart'; +import 'package:codec_utils/src/codecs/cbor/solana/cbor_sol_signature.dart'; import 'package:equatable/equatable.dart'; abstract class ACborTaggedObject extends Equatable { @@ -33,6 +35,10 @@ abstract class ACborTaggedObject extends Equatable { return CborEthSignature.fromCborMap(cborMap); case CborSpecialTag.ethSignRequest: return CborEthSignRequest.fromCborMap(cborMap); + case CborSpecialTag.solSignature: + return CborSolSignature.fromCborMap(cborMap); + case CborSpecialTag.solSignRequest: + return CborSolSignRequest.fromCborMap(cborMap); default: throw UnimplementedError('Unimplemented CBOR tag: ${cborSpecialTag}'); } diff --git a/lib/src/codecs/cbor/cbor_special_tag.dart b/lib/src/codecs/cbor/cbor_special_tag.dart index 4b3cfae..a1e4e54 100644 --- a/lib/src/codecs/cbor/cbor_special_tag.dart +++ b/lib/src/codecs/cbor/cbor_special_tag.dart @@ -5,10 +5,15 @@ enum CborSpecialTag { cryptoHDKey(type: 'crypto-hdkey', tag: 303), cryptoKeypath(type: 'crypto-keypath', tag: 304), cryptoCoinInfo(type: 'crypto-coin-info', tag: 305), + cryptoMultiAccounts(type: 'crypto-multi-accounts', tag: 1103), // ETH ethSignRequest(type: 'eth-sign-request', tag: 401), - ethSignature(type: 'eth-signature', tag: 402); + ethSignature(type: 'eth-signature', tag: 402), + + // SOL + solSignRequest(type: 'sol-sign-request', tag: 1101), + solSignature(type: 'sol-signature', tag: 1102); final String type; final int tag; diff --git a/lib/src/codecs/cbor/crypto/cbor_crypto_multi_accounts.dart b/lib/src/codecs/cbor/crypto/cbor_crypto_multi_accounts.dart new file mode 100644 index 0000000..d4d2600 --- /dev/null +++ b/lib/src/codecs/cbor/crypto/cbor_crypto_multi_accounts.dart @@ -0,0 +1,85 @@ +import 'dart:typed_data'; + +import 'package:cbor/cbor.dart'; +import 'package:codec_utils/src/codecs/cbor/a_cbor_tagged_object.dart'; +import 'package:codec_utils/src/codecs/cbor/cbor_special_tag.dart'; +import 'package:codec_utils/src/codecs/cbor/crypto/cbor_crypto_hd_key.dart'; + +/// For Solana, crypto-multi-accounts exposes the public keys. +/// This data may be used to generate the desired addresses. +/// Wallets like Solflare may retrieve and parse this data from the animated QR Code we display. +/// +/// There is a discrepancy between the Keystone documentation (link below) showing the parameter masterFingerprint as required +/// and our testing results which revealed Solflare actually only requires cryptoHDKeyList. +/// There is no information on how null values should be handled or what would happen if we received them. +/// +/// https://dev.keyst.one/docs/integration-tutorial-advanced/solana#connect-with-keystone +class CborCryptoMultiAccounts extends ACborTaggedObject { + static const CborSpecialTag cborSpecialTag = CborSpecialTag.cryptoMultiAccounts; + + /// A 4 bytes hex string indicates the current mnemonic, e.g. 'f23f9fd2' + final String? masterFingerprint; + + /// An array of extended public keys + final List cryptoHDKeyList; + + /// The device name, e.g. 'Keystone' + final String? device; + + /// The device id, e.g. '28475c8d80f6c06bafbe46a7d1750f3fcf2565f7' + final String? deviceId; + + /// The device firmware version, e.g. '1.0.2' + final String? deviceVersion; + + const CborCryptoMultiAccounts({ + required this.cryptoHDKeyList, + this.masterFingerprint, + this.device, + this.deviceId, + this.deviceVersion, + }); + + factory CborCryptoMultiAccounts.fromSerializedCbor(Uint8List serializedCbor) { + CborMap cborMap = cborDecode(serializedCbor) as CborMap; + return CborCryptoMultiAccounts.fromCborMap(cborMap); + } + + factory CborCryptoMultiAccounts.fromCborMap(CborMap cborMap) { + CborString? cborFingerprint = cborMap[const CborSmallInt(1)] as CborString?; + CborList? cborCryptoHDKeyList = cborMap[const CborSmallInt(2)] as CborList?; + CborString? cborDevice = cborMap[const CborSmallInt(3)] as CborString?; + CborString? cborDeviceId = cborMap[const CborSmallInt(4)] as CborString?; + CborString? cborVersion = cborMap[const CborSmallInt(5)] as CborString?; + + return CborCryptoMultiAccounts( + masterFingerprint: cborFingerprint?.toString(), + cryptoHDKeyList: cborCryptoHDKeyList?.whereType().map(CborCryptoHDKey.fromCborMap).toList() ?? [], + device: cborDevice?.toString(), + deviceId: cborDeviceId?.toString(), + deviceVersion: cborVersion?.toString(), + ); + } + + @override + CborMap toCborMap({required bool includeTagBool}) { + return CborMap.of( + { + if (masterFingerprint != null) const CborSmallInt(1): CborString(masterFingerprint!), + const CborSmallInt(2): CborList( + cryptoHDKeyList.map((CborCryptoHDKey cryptoHDKey) => cryptoHDKey.toCborMap(includeTagBool: true)).toList(), + ), + if (device != null) const CborSmallInt(3): CborString(device!), + if (deviceId != null) const CborSmallInt(4): CborString(deviceId!), + if (deviceVersion != null) const CborSmallInt(5): CborString(deviceVersion!), + }, + tags: includeTagBool ? [cborSpecialTag.tag] : [], + ); + } + + @override + CborSpecialTag getCborSpecialTag() => cborSpecialTag; + + @override + List get props => [masterFingerprint, cryptoHDKeyList, device, deviceId, deviceVersion]; +} diff --git a/lib/src/codecs/cbor/export.dart b/lib/src/codecs/cbor/export.dart index d668e1f..cb3521d 100644 --- a/lib/src/codecs/cbor/export.dart +++ b/lib/src/codecs/cbor/export.dart @@ -3,7 +3,11 @@ export 'cbor_special_tag.dart'; export 'crypto/cbor_crypto_coin_info.dart'; export 'crypto/cbor_crypto_hd_key.dart'; export 'crypto/cbor_crypto_keypath.dart'; +export 'crypto/cbor_crypto_multi_accounts.dart'; export 'crypto/metadata/cbor_path_component.dart'; export 'ethereum/cbor_eth_sign_request.dart'; export 'ethereum/cbor_eth_signature.dart'; export 'ethereum/metadata/cbor_eth_sign_data_type.dart'; +export 'solana/cbor_sol_sign_request.dart'; +export 'solana/cbor_sol_signature.dart'; +export 'solana/metadata/cbor_sol_sign_data_type.dart'; \ No newline at end of file diff --git a/lib/src/codecs/cbor/solana/cbor_sol_sign_request.dart b/lib/src/codecs/cbor/solana/cbor_sol_sign_request.dart new file mode 100644 index 0000000..ae52e4b --- /dev/null +++ b/lib/src/codecs/cbor/solana/cbor_sol_sign_request.dart @@ -0,0 +1,98 @@ +import 'dart:typed_data'; + +import 'package:cbor/cbor.dart'; +import 'package:codec_utils/src/codecs/cbor/a_cbor_tagged_object.dart'; +import 'package:codec_utils/src/codecs/cbor/cbor_special_tag.dart'; +import 'package:codec_utils/src/codecs/cbor/crypto/cbor_crypto_keypath.dart'; +import 'package:codec_utils/src/codecs/cbor/solana/metadata/cbor_sol_sign_data_type.dart'; + +/// Metadata for the signing request for Solana. +/// +/// There is a discrepancy between the Keystone documentation (the first link below) showing the parameter requestId as required +/// and actual implementation in their code which leaves it optional (the second link below). +/// There is no information on how null values should be handled or what would happen if we received them. +/// This implementation follows Keystone's actual implementation. +/// +/// https://dev.keyst.one/docs/integration-tutorial-advanced/solana#genereate-the-sign-request +/// https://github.com/KeystoneHQ/keystone-sdk-base/blob/master/packages/ur-registry-sol/src/SolSignRequest.ts +class CborSolSignRequest extends ACborTaggedObject { + static const CborSpecialTag cborSpecialTag = CborSpecialTag.solSignRequest; + + /// The unsigned transaction data, in hex string. + final Uint8List signData; + + /// The type of data to be signed, listed in [SolSignDataType]. + final CborSolSignDataType dataType; + + /// The key path of the private key to sign the data. + final CborCryptoKeypath derivationPath; + + /// The identifier for signing request. + final Uint8List? requestId; + + /// The address for request this signing. + final Uint8List? address; + + /// The origin of this sign request, like wallet name. + final String? origin; + + const CborSolSignRequest({ + required this.signData, + required this.dataType, + required this.derivationPath, + required this.requestId, + this.address, + this.origin, + }); + + factory CborSolSignRequest.fromSerializedCbor(Uint8List serializedCbor) { + CborMap cborMap = cborDecode(serializedCbor) as CborMap; + return CborSolSignRequest.fromCborMap(cborMap); + } + + factory CborSolSignRequest.fromCborMap(CborMap cborMap) { + CborBytes? cborRequestId = cborMap[const CborSmallInt(1)] as CborBytes?; + CborBytes cborSignData = cborMap[const CborSmallInt(2)] as CborBytes; + CborMap cborDerivationPath = cborMap[const CborSmallInt(3)] as CborMap; + CborBytes? cborAddress = cborMap[const CborSmallInt(4)] as CborBytes?; + CborString? cborOrigin = cborMap[const CborSmallInt(5)] as CborString?; + CborSmallInt cborSignType = cborMap[const CborSmallInt(6)] as CborSmallInt; + + return CborSolSignRequest( + requestId: Uint8List.fromList(cborRequestId!.bytes), + signData: Uint8List.fromList(cborSignData.bytes), + dataType: CborSolSignDataType.fromCborIndex(cborSignType.value), + derivationPath: CborCryptoKeypath.fromCborMap(cborDerivationPath), + address: cborAddress != null ? Uint8List.fromList(cborAddress.bytes) : null, + origin: cborOrigin?.toString(), + ); + } + + @override + CborMap toCborMap({required bool includeTagBool}) { + return CborMap.of( + { + if (requestId != null) const CborSmallInt(1): CborBytes(Uint8List.fromList(requestId!), tags: [CborSpecialTag.uuid.tag]), + const CborSmallInt(2): CborBytes(Uint8List.fromList(signData)), + const CborSmallInt(3): derivationPath.toCborMap(includeTagBool: true), + if (address != null) const CborSmallInt(4): CborBytes(address!), + if (origin != null) const CborSmallInt(5): CborString(origin!), + const CborSmallInt(6): CborSmallInt(dataType.cborIndex), + }, + tags: includeTagBool ? [cborSpecialTag.tag] : [], + ); + } + + @override + CborSpecialTag getCborSpecialTag() => cborSpecialTag; + + @override + List get props => [ + requestId, + signData, + derivationPath, + address, + origin, + dataType, + ]; +} diff --git a/lib/src/codecs/cbor/solana/cbor_sol_signature.dart b/lib/src/codecs/cbor/solana/cbor_sol_signature.dart new file mode 100644 index 0000000..d7ef9a5 --- /dev/null +++ b/lib/src/codecs/cbor/solana/cbor_sol_signature.dart @@ -0,0 +1,61 @@ +import 'dart:typed_data'; + +import 'package:cbor/cbor.dart'; +import 'package:codec_utils/src/codecs/cbor/a_cbor_tagged_object.dart'; +import 'package:codec_utils/src/codecs/cbor/cbor_special_tag.dart'; + +/// Metadata for the signature response for Solana. +/// +/// There is a discrepancy between the Keystone documentation (the first link below) showing the parameter requestId as required +/// and actual implementation in their code which leaves it optional (the second link below). +/// There is no information on how null values should be handled or what would happen if we received them. +/// This implementation follows Keystone's actual implementation. +/// +/// https://dev.keyst.one/docs/integration-tutorial-advanced/solana#extract-signature +/// https://github.com/KeystoneHQ/keystone-sdk-base/blob/master/packages/ur-registry-sol/src/SolSignature.ts +class CborSolSignature extends ACborTaggedObject { + static const CborSpecialTag cborSpecialTag = CborSpecialTag.solSignature; + + /// The requestId from sign request + final Uint8List? requestId; + + /// The serialized signature in hex string + final Uint8List signature; + + const CborSolSignature({ + required this.requestId, + required this.signature, + }); + + factory CborSolSignature.fromSerializedCbor(Uint8List serializedCbor) { + CborMap cborMap = cborDecode(serializedCbor) as CborMap; + return CborSolSignature.fromCborMap(cborMap); + } + + factory CborSolSignature.fromCborMap(CborMap cborMap) { + CborBytes? cborRequestId = cborMap[const CborSmallInt(1)] as CborBytes?; + CborBytes cborSignature = cborMap[const CborSmallInt(2)] as CborBytes; + + return CborSolSignature( + requestId: cborRequestId != null ? Uint8List.fromList(cborRequestId.bytes) : null, + signature: Uint8List.fromList(cborSignature.bytes), + ); + } + + @override + CborMap toCborMap({required bool includeTagBool}) { + return CborMap.of( + { + const CborSmallInt(1): CborBytes(requestId!, tags: [CborSpecialTag.uuid.tag]), + const CborSmallInt(2): CborBytes(signature), + }, + tags: includeTagBool ? [cborSpecialTag.tag] : [], + ); + } + + @override + CborSpecialTag getCborSpecialTag() => cborSpecialTag; + + @override + List get props => [requestId, signature]; +} diff --git a/lib/src/codecs/cbor/solana/metadata/cbor_sol_sign_data_type.dart b/lib/src/codecs/cbor/solana/metadata/cbor_sol_sign_data_type.dart new file mode 100644 index 0000000..afd7aec --- /dev/null +++ b/lib/src/codecs/cbor/solana/metadata/cbor_sol_sign_data_type.dart @@ -0,0 +1,14 @@ +/// Metadata for the signing request for Solana. +/// https://dev.keyst.one/docs/integration-tutorial-advanced/solana#genereate-the-sign-request +enum CborSolSignDataType { + transaction(1), + message(2); + + final int cborIndex; + + const CborSolSignDataType(this.cborIndex); + + factory CborSolSignDataType.fromCborIndex(int cborIndex) { + return CborSolSignDataType.values.firstWhere((CborSolSignDataType type) => type.cborIndex == cborIndex); + } +} diff --git a/lib/src/codecs/compact_u16/compact_u16_decoder.dart b/lib/src/codecs/compact_u16/compact_u16_decoder.dart new file mode 100644 index 0000000..7c18c39 --- /dev/null +++ b/lib/src/codecs/compact_u16/compact_u16_decoder.dart @@ -0,0 +1,40 @@ +import 'package:codec_utils/src/codecs/byte_reader/byte_reader.dart'; + +/// CompactU16 is a compact, variable-length encoding for 16-bit unsigned integers (u16). +/// It is designed to save space when serializing small integers by using fewer bytes for smaller values. +/// The implementation is inspired by the variable-length quantity (VLQ) encoding used in formats like Protocol Buffers. +/// +/// Code partially based on: +/// https://github.com/espresso-cash/espresso-cash-public/blob/master/packages/solana/lib/src/encoder/compact_u16.dart +/// https://github.com/anza-xyz/agave/blob/v2.1.13/short-vec/src/lib.rs +class CompactU16Decoder { + static const int _maxCompactU16EncodingLength = 3; + + /// Decodes the first actual integer value from [byteReader], starting at its current [offset]. + static int decode(ByteReader byteReader) { + int value = 0; + int size = 0; + int shift = 0; + try { + for (int i = 0; i < _maxCompactU16EncodingLength; i++) { + int elem = byteReader.shiftRight(); + size++; + if (elem == 0 && i != 0) { + throw Exception('Zero byte found beyond first position'); + } + if (i == _maxCompactU16EncodingLength - 1 && (elem & 0x80) != 0) { + throw Exception('Attempted to read past the third byte'); + } + value |= (elem & 0x7F) << shift; + shift += 7; + if ((elem & 0x80) == 0) { + break; + } + } + } catch (e) { + byteReader.shiftLeftBy(size); + rethrow; + } + return value; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 9267116..a73fcf0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: codec_utils description: "Dart package containing utility methods for data encoding and decoding" -version: 0.0.6 +version: 0.0.7 publish_to: none environment: diff --git a/test/unit/codecs/base/base58_codec_test.dart b/test/unit/codecs/base/base58_codec_test.dart index 579896e..2a2aabc 100644 --- a/test/unit/codecs/base/base58_codec_test.dart +++ b/test/unit/codecs/base/base58_codec_test.dart @@ -47,6 +47,19 @@ void main() { expect(actualDecodedData, expectedDecodedData); }); + test("Should [return String] composed entirely of '1' encoded by Base58", () { + // Arrange + String actualBase58 = '11111111111111111111111111111111'; + + // Act + Uint8List actualDecodedData = Base58Codec.decode(actualBase58); + + // Assert + Uint8List expectedDecodedData = base64Decode('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='); + + expect(actualDecodedData, expectedDecodedData); + }); + test('Should [return String] encoded by Base58 (with checksum)', () { // Arrange String actualBase58 = '4nNW8qCqV3i7VY'; @@ -59,5 +72,13 @@ void main() { expect(actualDecodedData, expectedDecodedData); }); + + test('Should [throw FormatException] when a String encoded by Base58 has an illegal character', () { + // Arrange + String actualBase58 = '4nNW8qCqV3i7VYl'; + + // Assert + expect(() => Base58Codec.decode(actualBase58), throwsException); + }); }); } diff --git a/test/unit/codecs/byte_reader/byte_reader_test.dart b/test/unit/codecs/byte_reader/byte_reader_test.dart new file mode 100644 index 0000000..68d58a2 --- /dev/null +++ b/test/unit/codecs/byte_reader/byte_reader_test.dart @@ -0,0 +1,91 @@ +// ignore_for_file: cascade_invocations + +import 'dart:typed_data'; + +import 'package:codec_utils/src/codecs/byte_reader/byte_reader.dart'; +import 'package:test/test.dart'; + +void main() { + group('Tests of ByteReader.shiftLeftBy()', () { + test('Should [decrement offset by (2)]', () { + // Arrange + ByteReader actualByteReader = ByteReader(Uint8List.fromList([0x01, 0x02, 0x03, 0x04])); + + // Act + actualByteReader.shiftRightBy(3); + actualByteReader.shiftLeftBy(2); + int actualOffset = actualByteReader.offset; + + // Assert + int expectedOffset = 1; + + expect(actualOffset, expectedOffset); + }); + + test('Should [throw Exception] if shifted left past end of data', () { + // Arrange + ByteReader actualByteReader = ByteReader(Uint8List.fromList([0x01, 0x02, 0x03, 0x04])); + + // Act + actualByteReader.shiftRightBy(3); + + // Assert + expect(() => actualByteReader.shiftLeftBy(4), throwsException); + }); + }); + + group('Tests of ByteReader.shiftRight()', () { + test('Should [return element at current offset] and [increment offset]', () { + // Arrange + ByteReader actualByteReader = ByteReader(Uint8List.fromList([0x01, 0x02, 0x03, 0x04])); + + // Act + int actualReadByte = actualByteReader.shiftRight(); + int actualOffset = actualByteReader.offset; + + // Assert + int expectedReadByte = 0x01; + int expectedOffset = 1; + + expect(actualReadByte, expectedReadByte); + expect(actualOffset, expectedOffset); + }); + + test('Should [throw Exception] if shifted right past end of data', () { + // Arrange + ByteReader actualByteReader = ByteReader(Uint8List.fromList([0x01])); + + // Act + actualByteReader.shiftRight(); + + // Assert + expect(() => actualByteReader.shiftRight(), throwsException); + }); + }); + + group('Tests of ByteReader.shiftRightBy()', () { + test('Should [return (2) elements starting at current offset] and [increment offset by (2)]', () { + // Arrange + ByteReader actualByteReader = ByteReader(Uint8List.fromList([0x01, 0x02, 0x03, 0x04])); + + // Act + Uint8List actualShiftedBytes = actualByteReader.shiftRightBy(2); + int actualOffset = actualByteReader.offset; + + // Assert + Uint8List expectedShiftedBytes = Uint8List.fromList([0x01, 0x02]); + int expectedOffset = 2; + + expect(actualShiftedBytes, expectedShiftedBytes); + expect(actualOffset, expectedOffset); + }); + + test('Should [throw Exception] if shifted right past end of data', () { + // Arrange + ByteReader actualByteReader = ByteReader(Uint8List.fromList([0x01, 0x02, 0x03, 0x04])); + + // Assert + expect(() => actualByteReader.shiftRightBy(5), throwsException); + }); + }); +} diff --git a/test/unit/codecs/cbor/crypto/cbor_crypto_multi_accounts_test.dart b/test/unit/codecs/cbor/crypto/cbor_crypto_multi_accounts_test.dart new file mode 100644 index 0000000..84b981e --- /dev/null +++ b/test/unit/codecs/cbor/crypto/cbor_crypto_multi_accounts_test.dart @@ -0,0 +1,440 @@ +import 'dart:typed_data'; + +import 'package:cbor/cbor.dart'; +import 'package:codec_utils/src/codecs/cbor/cbor_special_tag.dart'; +import 'package:codec_utils/src/codecs/cbor/crypto/cbor_crypto_hd_key.dart'; +import 'package:codec_utils/src/codecs/cbor/crypto/cbor_crypto_keypath.dart'; +import 'package:codec_utils/src/codecs/cbor/crypto/cbor_crypto_multi_accounts.dart'; +import 'package:codec_utils/src/codecs/cbor/crypto/metadata/cbor_path_component.dart'; +import 'package:codec_utils/src/codecs/hex/hex_codec.dart'; +import 'package:test/test.dart'; + +void main() { + group('Tests of CborCryptoMultiAccounts.fromSerializedCbor()', () { + test('Should [return CborCryptoMultiAccounts] from serialized CBOR bytes (WITH tag)', () { + // Arrange + Uint8List actualSerializedCborBytes = HexCodec.decode( + 'd9044fa5016865393138316366330281d9012fa203582102eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b06d90130a10188182cf51901f5f500f500f503686b657973746f6e65047828323834373563386438306636633036626166626534366137643137353066336663663235363566370565312e302e32'); + + // Act + CborCryptoMultiAccounts actualCborCryptoMultiAccounts = CborCryptoMultiAccounts.fromSerializedCbor(actualSerializedCborBytes); + + // Assert + CborCryptoMultiAccounts expectedCborCryptoMultiAccounts = CborCryptoMultiAccounts( + masterFingerprint: 'e9181cf3', + cryptoHDKeyList: [ + CborCryptoHDKey( + isMaster: false, + isPrivate: false, + keyData: HexCodec.decode('02eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b'), + origin: const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + )), + ], + device: 'keystone', + deviceId: '28475c8d80f6c06bafbe46a7d1750f3fcf2565f7', + deviceVersion: '1.0.2', + ); + + expect(actualCborCryptoMultiAccounts, expectedCborCryptoMultiAccounts); + }); + + test('Should [return CborCryptoMultiAccounts] from serialized CBOR bytes (WITHOUT tag)', () { + // Arrange + Uint8List actualSerializedCborBytes = HexCodec.decode( + 'a5016865393138316366330281d9012fa203582102eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b06d90130a10188182cf51901f5f500f500f503686b657973746f6e65047828323834373563386438306636633036626166626534366137643137353066336663663235363566370565312e302e32'); + + // Act + CborCryptoMultiAccounts actualCborCryptoMultiAccounts = CborCryptoMultiAccounts.fromSerializedCbor(actualSerializedCborBytes); + + // Assert + CborCryptoMultiAccounts expectedCborCryptoMultiAccounts = CborCryptoMultiAccounts( + masterFingerprint: 'e9181cf3', + cryptoHDKeyList: [ + CborCryptoHDKey( + isMaster: false, + isPrivate: false, + keyData: HexCodec.decode('02eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b'), + origin: const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + )), + ], + device: 'keystone', + deviceId: '28475c8d80f6c06bafbe46a7d1750f3fcf2565f7', + deviceVersion: '1.0.2', + ); + + expect(actualCborCryptoMultiAccounts, expectedCborCryptoMultiAccounts); + }); + }); + + group('Tests of CborCryptoMultiAccounts.fromCborMap()', () { + test('Should [return CborCryptoMultiAccounts] from CborMap (WITH tag)', () { + // Arrange + CborMap actualCborMap = CborMap( + { + const CborSmallInt(1): CborString('e9181cf3'), + const CborSmallInt(2): CborList([ + CborMap( + { + const CborSmallInt(1): const CborBool(false), + const CborSmallInt(2): const CborBool(false), + const CborSmallInt(3): CborBytes(HexCodec.decode('02eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b')), + const CborSmallInt(6): CborMap( + { + const CborSmallInt(1): CborList([ + const CborSmallInt(44), + const CborBool(true), + const CborSmallInt(501), + const CborBool(true), + const CborSmallInt(0), + const CborBool(true), + const CborSmallInt(0), + const CborBool(true), + ]), + }, + tags: [CborSpecialTag.cryptoKeypath.tag], + ), + }, + tags: [CborSpecialTag.cryptoHDKey.tag], + ), + ]), + const CborSmallInt(3): CborString('keystone'), + const CborSmallInt(4): CborString('28475c8d80f6c06bafbe46a7d1750f3fcf2565f7'), + const CborSmallInt(5): CborString('1.0.2'), + }, + tags: [CborSpecialTag.cryptoMultiAccounts.tag], + ); + + // Act + CborCryptoMultiAccounts actualCborCryptoMultiAccounts = CborCryptoMultiAccounts.fromCborMap(actualCborMap); + + // Assert + CborCryptoMultiAccounts expectedCborCryptoMultiAccounts = CborCryptoMultiAccounts( + masterFingerprint: 'e9181cf3', + cryptoHDKeyList: [ + CborCryptoHDKey( + isMaster: false, + isPrivate: false, + keyData: HexCodec.decode('02eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b'), + origin: const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + )), + ], + device: 'keystone', + deviceId: '28475c8d80f6c06bafbe46a7d1750f3fcf2565f7', + deviceVersion: '1.0.2', + ); + + expect(actualCborCryptoMultiAccounts, expectedCborCryptoMultiAccounts); + }); + + test('Should [return CborCryptoMultiAccounts] from CborMap (WITHOUT tag)', () { + // Arrange + CborMap actualCborMap = CborMap( + { + const CborSmallInt(1): CborString('e9181cf3'), + const CborSmallInt(2): CborList([ + CborMap( + { + const CborSmallInt(1): const CborBool(false), + const CborSmallInt(2): const CborBool(false), + const CborSmallInt(3): CborBytes(HexCodec.decode('02eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b')), + const CborSmallInt(6): CborMap( + { + const CborSmallInt(1): CborList([ + const CborSmallInt(44), + const CborBool(true), + const CborSmallInt(501), + const CborBool(true), + const CborSmallInt(0), + const CborBool(true), + const CborSmallInt(0), + const CborBool(true), + ]), + }, + tags: [CborSpecialTag.cryptoKeypath.tag], + ), + }, + tags: [CborSpecialTag.cryptoHDKey.tag], + ), + ]), + const CborSmallInt(3): CborString('keystone'), + const CborSmallInt(4): CborString('28475c8d80f6c06bafbe46a7d1750f3fcf2565f7'), + const CborSmallInt(5): CborString('1.0.2'), + }, + tags: [], + ); + + // Act + CborCryptoMultiAccounts actualCborCryptoMultiAccounts = CborCryptoMultiAccounts.fromCborMap(actualCborMap); + + // Assert + CborCryptoMultiAccounts expectedCborCryptoMultiAccounts = CborCryptoMultiAccounts( + masterFingerprint: 'e9181cf3', + cryptoHDKeyList: [ + CborCryptoHDKey( + isMaster: false, + isPrivate: false, + keyData: HexCodec.decode('02eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b'), + origin: const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + )), + ], + device: 'keystone', + deviceId: '28475c8d80f6c06bafbe46a7d1750f3fcf2565f7', + deviceVersion: '1.0.2', + ); + + expect(actualCborCryptoMultiAccounts, expectedCborCryptoMultiAccounts); + }); + }); + group('Tests of CborCryptoMultiAccounts.toCborMap()', () { + test('Should [return CborMap] from CborCryptoMultiAccounts (WITH tag)', () { + // Arrange + CborCryptoMultiAccounts actualCborCryptoMultiAccounts = CborCryptoMultiAccounts( + masterFingerprint: 'e9181cf3', + cryptoHDKeyList: [ + CborCryptoHDKey( + isMaster: false, + isPrivate: false, + keyData: HexCodec.decode('02eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b'), + origin: const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + )), + ], + device: 'keystone', + deviceId: '28475c8d80f6c06bafbe46a7d1750f3fcf2565f7', + deviceVersion: '1.0.2', + ); + + // Act + CborMap actualCborMap = actualCborCryptoMultiAccounts.toCborMap(includeTagBool: true); + + // Assert + CborMap expectedCborMap = CborMap( + { + const CborSmallInt(1): CborString('e9181cf3'), + const CborSmallInt(2): CborList([ + CborMap( + { + const CborSmallInt(3): CborBytes(HexCodec.decode('02eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b')), + const CborSmallInt(6): CborMap( + { + const CborSmallInt(1): CborList([ + const CborSmallInt(44), + const CborBool(true), + const CborSmallInt(501), + const CborBool(true), + const CborSmallInt(0), + const CborBool(true), + const CborSmallInt(0), + const CborBool(true), + ]), + }, + tags: [CborSpecialTag.cryptoKeypath.tag], + ), + }, + tags: [CborSpecialTag.cryptoHDKey.tag], + ), + ]), + const CborSmallInt(3): CborString('keystone'), + const CborSmallInt(4): CborString('28475c8d80f6c06bafbe46a7d1750f3fcf2565f7'), + const CborSmallInt(5): CborString('1.0.2'), + }, + tags: [CborSpecialTag.cryptoMultiAccounts.tag], + ); + + expect(actualCborMap, expectedCborMap); + }); + + test('Should [return CborMap] from CborCryptoMultiAccounts (WITHOUT tag)', () { + // Arrange + CborCryptoMultiAccounts actualCborCryptoMultiAccounts = CborCryptoMultiAccounts( + masterFingerprint: 'e9181cf3', + cryptoHDKeyList: [ + CborCryptoHDKey( + isMaster: false, + isPrivate: false, + keyData: HexCodec.decode('02eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b'), + origin: const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + )), + ], + device: 'keystone', + deviceId: '28475c8d80f6c06bafbe46a7d1750f3fcf2565f7', + deviceVersion: '1.0.2', + ); + + // Act + CborMap actualCborMap = actualCborCryptoMultiAccounts.toCborMap(includeTagBool: false); + + // Assert + CborMap expectedCborMap = CborMap( + { + const CborSmallInt(1): CborString('e9181cf3'), + const CborSmallInt(2): CborList([ + CborMap( + { + const CborSmallInt(3): CborBytes(HexCodec.decode('02eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b')), + const CborSmallInt(6): CborMap( + { + const CborSmallInt(1): CborList([ + const CborSmallInt(44), + const CborBool(true), + const CborSmallInt(501), + const CborBool(true), + const CborSmallInt(0), + const CborBool(true), + const CborSmallInt(0), + const CborBool(true), + ]), + }, + tags: [CborSpecialTag.cryptoKeypath.tag], + ), + }, + tags: [CborSpecialTag.cryptoHDKey.tag], + ), + ]), + const CborSmallInt(3): CborString('keystone'), + const CborSmallInt(4): CborString('28475c8d80f6c06bafbe46a7d1750f3fcf2565f7'), + const CborSmallInt(5): CborString('1.0.2'), + }, + tags: [CborSpecialTag.cryptoMultiAccounts.tag], + ); + + expect(actualCborMap, expectedCborMap); + }); + }); + + group('Tests of CborCryptoMultiAccounts.toSerializedCbor()', () { + test('Should [return CborCryptoMultiAccounts] from serialized CBOR bytes (WITH tag)', () { + // Arrange + CborCryptoMultiAccounts actualCborCryptoMultiAccounts = CborCryptoMultiAccounts( + masterFingerprint: 'e9181cf3', + cryptoHDKeyList: [ + CborCryptoHDKey( + isMaster: false, + isPrivate: false, + keyData: HexCodec.decode('02eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b'), + origin: const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + )), + ], + device: 'keystone', + deviceId: '28475c8d80f6c06bafbe46a7d1750f3fcf2565f7', + deviceVersion: '1.0.2', + ); + + // Act + Uint8List actualSerializedCborBytes = actualCborCryptoMultiAccounts.toSerializedCbor(includeTagBool: true); + + // Assert + Uint8List expectedSerializedCborBytes = HexCodec.decode( + 'd9044fa5016865393138316366330281d9012fa203582102eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b06d90130a10188182cf51901f5f500f500f503686b657973746f6e65047828323834373563386438306636633036626166626534366137643137353066336663663235363566370565312e302e32'); + + expect(actualSerializedCborBytes, expectedSerializedCborBytes); + }); + + test('Should [return CborCryptoMultiAccounts] from serialized CBOR bytes (WITHOUT tag)', () { + // Arrange + CborCryptoMultiAccounts actualCborCryptoMultiAccounts = CborCryptoMultiAccounts( + masterFingerprint: 'e9181cf3', + cryptoHDKeyList: [ + CborCryptoHDKey( + isMaster: false, + isPrivate: false, + keyData: HexCodec.decode('02eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b'), + origin: const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + )), + ], + device: 'keystone', + deviceId: '28475c8d80f6c06bafbe46a7d1750f3fcf2565f7', + deviceVersion: '1.0.2', + ); + + // Act + Uint8List actualSerializedCborBytes = actualCborCryptoMultiAccounts.toSerializedCbor(includeTagBool: false); + + // Assert + Uint8List expectedSerializedCborBytes = HexCodec.decode( + 'a5016865393138316366330281d9012fa203582102eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b06d90130a10188182cf51901f5f500f500f503686b657973746f6e65047828323834373563386438306636633036626166626534366137643137353066336663663235363566370565312e302e32'); + + expect(actualSerializedCborBytes, expectedSerializedCborBytes); + }); + }); + group('Tests of CborCryptoMultiAccounts.getCborSpecialTag()', () { + test('Should [return CborSpecialTag.cryptoMultiAccounts] from CborCryptoMultiAccounts', () { + // Arrange + CborCryptoMultiAccounts actualCborCryptoMultiAccounts = CborCryptoMultiAccounts( + masterFingerprint: 'e9181cf3', + cryptoHDKeyList: [ + CborCryptoHDKey( + isMaster: false, + isPrivate: false, + keyData: HexCodec.decode('02eae4b876a8696134b868f88cc2f51f715f2dbedb7446b8e6edf3d4541c4eb67b'), + origin: const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + )), + ], + device: 'keystone', + deviceId: '28475c8d80f6c06bafbe46a7d1750f3fcf2565f7', + deviceVersion: '1.0.2', + ); + + // Act + CborSpecialTag actualCborSpecialTag = actualCborCryptoMultiAccounts.getCborSpecialTag(); + + // Assert + CborSpecialTag expectedCborSpecialTag = CborSpecialTag.cryptoMultiAccounts; + + expect(actualCborSpecialTag, expectedCborSpecialTag); + }); + }); +} diff --git a/test/unit/codecs/cbor/solana/cbor_sol_sign_request_test.dart b/test/unit/codecs/cbor/solana/cbor_sol_sign_request_test.dart new file mode 100644 index 0000000..9634458 --- /dev/null +++ b/test/unit/codecs/cbor/solana/cbor_sol_sign_request_test.dart @@ -0,0 +1,358 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:cbor/cbor.dart'; +import 'package:codec_utils/src/codecs/cbor/cbor_special_tag.dart'; +import 'package:codec_utils/src/codecs/cbor/crypto/cbor_crypto_keypath.dart'; +import 'package:codec_utils/src/codecs/cbor/crypto/metadata/cbor_path_component.dart'; +import 'package:codec_utils/src/codecs/cbor/solana/cbor_sol_sign_request.dart'; +import 'package:codec_utils/src/codecs/cbor/solana/metadata/cbor_sol_sign_data_type.dart'; +import 'package:codec_utils/src/codecs/hex/hex_codec.dart'; +import 'package:test/test.dart'; + +void main() { + group('Tests of CborSolSignRequest.fromSerializedCbor()', () { + test('Should [return CborSolSignRequest] from serialized CBOR bytes (WITH tag)', () { + // Arrange + Uint8List actualSerializedCborBytes = HexCodec.decode( + 'd9044da501d825509b1deb4d3b7d4bad9bdd2b0d7b3dcb6d02589601000103c8d842a2f17fd7aab608ce2ea535a6e958dffa20caf669b347b911c4171965530f957620b228bae2b94c82ddd4c093983a67365555b737ec7ddc1117e61c72e0000000000000000000000000000000000000000000000000000000000000000010295cc2f1f39f3604718496ea00676d6a72ec66ad09d926e3ece34f565f18d201020200010c0200000000e1f5050000000003d90130a20188182cf51901f5f500f500f5021a075bcd150568736f6c666c6172650601'); + + // Act + CborSolSignRequest actualCborSolSignRequest = CborSolSignRequest.fromSerializedCbor(actualSerializedCborBytes); + + // Assert + CborSolSignRequest expectedCborSolSignRequest = CborSolSignRequest( + requestId: base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), + signData: base64Decode( + 'AQABA8jYQqLxf9eqtgjOLqU1pulY3/ogyvZps0e5EcQXGWVTD5V2ILIouuK5TILd1MCTmDpnNlVVtzfsfdwRF+YccuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABApXMLx8582BHGEluoAZ21qcuxmrQnZJuPs409WXxjSAQICAAEMAgAAAADh9QUAAAAA', + ), + derivationPath: const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + sourceFingerprint: 123456789, + ), + origin: 'solflare', + dataType: CborSolSignDataType.transaction, + ); + + expect(actualCborSolSignRequest, expectedCborSolSignRequest); + }); + + test('Should [return CborSolSignRequest] from serialized CBOR bytes (WITHOUT tag)', () { + // Arrange + Uint8List actualSerializedCborBytes = HexCodec.decode( + 'a501d825509b1deb4d3b7d4bad9bdd2b0d7b3dcb6d02589601000103c8d842a2f17fd7aab608ce2ea535a6e958dffa20caf669b347b911c4171965530f957620b228bae2b94c82ddd4c093983a67365555b737ec7ddc1117e61c72e0000000000000000000000000000000000000000000000000000000000000000010295cc2f1f39f3604718496ea00676d6a72ec66ad09d926e3ece34f565f18d201020200010c0200000000e1f5050000000003d90130a20188182cf51901f5f500f500f5021a075bcd150568736f6c666c6172650601'); + + // Act + CborSolSignRequest actualCborSolSignRequest = CborSolSignRequest.fromSerializedCbor(actualSerializedCborBytes); + + // Assert + CborSolSignRequest expectedCborSolSignRequest = CborSolSignRequest( + requestId: base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), + signData: base64Decode( + 'AQABA8jYQqLxf9eqtgjOLqU1pulY3/ogyvZps0e5EcQXGWVTD5V2ILIouuK5TILd1MCTmDpnNlVVtzfsfdwRF+YccuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABApXMLx8582BHGEluoAZ21qcuxmrQnZJuPs409WXxjSAQICAAEMAgAAAADh9QUAAAAA', + ), + derivationPath: const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + sourceFingerprint: 123456789, + ), + origin: 'solflare', + dataType: CborSolSignDataType.transaction, + ); + + expect(actualCborSolSignRequest, expectedCborSolSignRequest); + }); + }); + + group('Tests of CborSolSignRequest.fromCborMap()', () { + test('Should [return CborSolSignRequest] from CborMap (WITH tag)', () { + // Arrange + CborMap actualCborMap = CborMap( + { + const CborSmallInt(1): CborBytes(base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), tags: [CborSpecialTag.uuid.tag]), + const CborSmallInt(2): CborBytes(base64Decode( + 'AQABA8jYQqLxf9eqtgjOLqU1pulY3/ogyvZps0e5EcQXGWVTD5V2ILIouuK5TILd1MCTmDpnNlVVtzfsfdwRF+YccuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABApXMLx8582BHGEluoAZ21qcuxmrQnZJuPs409WXxjSAQICAAEMAgAAAADh9QUAAAAA', + )), + const CborSmallInt(3): const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + sourceFingerprint: 123456789, + ).toCborMap(includeTagBool: true), + const CborSmallInt(5): CborString('solflare'), + const CborSmallInt(6): CborSmallInt(CborSolSignDataType.transaction.cborIndex), + }, + tags: [CborSpecialTag.solSignRequest.tag], + ); + + // Act + CborSolSignRequest actualCborSolSignRequest = CborSolSignRequest.fromCborMap(actualCborMap); + + // Assert + CborSolSignRequest expectedCborSolSignRequest = CborSolSignRequest( + requestId: base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), + signData: base64Decode( + 'AQABA8jYQqLxf9eqtgjOLqU1pulY3/ogyvZps0e5EcQXGWVTD5V2ILIouuK5TILd1MCTmDpnNlVVtzfsfdwRF+YccuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABApXMLx8582BHGEluoAZ21qcuxmrQnZJuPs409WXxjSAQICAAEMAgAAAADh9QUAAAAA'), + derivationPath: const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + sourceFingerprint: 123456789, + ), + origin: 'solflare', + dataType: CborSolSignDataType.transaction, + ); + + expect(actualCborSolSignRequest, expectedCborSolSignRequest); + }); + + test('Should [return CborSolSignRequest] from CborMap (WITHOUT tag)', () { + // Arrange + CborMap actualCborMap = CborMap( + { + const CborSmallInt(1): CborBytes(base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), tags: [CborSpecialTag.uuid.tag]), + const CborSmallInt(2): CborBytes(base64Decode( + 'AQABA8jYQqLxf9eqtgjOLqU1pulY3/ogyvZps0e5EcQXGWVTD5V2ILIouuK5TILd1MCTmDpnNlVVtzfsfdwRF+YccuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABApXMLx8582BHGEluoAZ21qcuxmrQnZJuPs409WXxjSAQICAAEMAgAAAADh9QUAAAAA', + )), + const CborSmallInt(3): const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + sourceFingerprint: 123456789, + ).toCborMap(includeTagBool: true), + const CborSmallInt(5): CborString('solflare'), + const CborSmallInt(6): CborSmallInt(CborSolSignDataType.transaction.cborIndex), + }, + ); + + // Act + CborSolSignRequest actualCborSolSignRequest = CborSolSignRequest.fromCborMap(actualCborMap); + + // Assert + CborSolSignRequest expectedCborSolSignRequest = CborSolSignRequest( + requestId: base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), + signData: base64Decode( + 'AQABA8jYQqLxf9eqtgjOLqU1pulY3/ogyvZps0e5EcQXGWVTD5V2ILIouuK5TILd1MCTmDpnNlVVtzfsfdwRF+YccuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABApXMLx8582BHGEluoAZ21qcuxmrQnZJuPs409WXxjSAQICAAEMAgAAAADh9QUAAAAA'), + derivationPath: const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + sourceFingerprint: 123456789, + ), + origin: 'solflare', + dataType: CborSolSignDataType.transaction, + ); + + expect(actualCborSolSignRequest, expectedCborSolSignRequest); + }); + }); + + group('Tests of CborSolSignRequest.toSerializedCbor()', () { + test('Should [return serialized CBOR bytes] from CborSolSignRequest (WITH tag)', () { + // Arrange + CborSolSignRequest actualCborSolSignRequest = CborSolSignRequest( + requestId: base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), + signData: base64Decode( + 'AQABA8jYQqLxf9eqtgjOLqU1pulY3/ogyvZps0e5EcQXGWVTD5V2ILIouuK5TILd1MCTmDpnNlVVtzfsfdwRF+YccuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABApXMLx8582BHGEluoAZ21qcuxmrQnZJuPs409WXxjSAQICAAEMAgAAAADh9QUAAAAA', + ), + derivationPath: const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + sourceFingerprint: 123456789, + ), + origin: 'solflare', + dataType: CborSolSignDataType.transaction, + ); + + // Act + Uint8List actualSerializedCborBytes = actualCborSolSignRequest.toSerializedCbor(includeTagBool: true); + + // Assert + Uint8List expectedSerializedCborBytes = HexCodec.decode( + 'd9044da501d825509b1deb4d3b7d4bad9bdd2b0d7b3dcb6d02589601000103c8d842a2f17fd7aab608ce2ea535a6e958dffa20caf669b347b911c4171965530f957620b228bae2b94c82ddd4c093983a67365555b737ec7ddc1117e61c72e0000000000000000000000000000000000000000000000000000000000000000010295cc2f1f39f3604718496ea00676d6a72ec66ad09d926e3ece34f565f18d201020200010c0200000000e1f5050000000003d90130a20188182cf51901f5f500f500f5021a075bcd150568736f6c666c6172650601', + ); + + expect(actualSerializedCborBytes, expectedSerializedCborBytes); + }); + + test('Should [return serialized CBOR bytes] from CborSolSignRequest (WITHOUT tag)', () { + // Arrange + CborSolSignRequest actualCborSolSignRequest = CborSolSignRequest( + requestId: base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), + signData: base64Decode( + 'AQABA8jYQqLxf9eqtgjOLqU1pulY3/ogyvZps0e5EcQXGWVTD5V2ILIouuK5TILd1MCTmDpnNlVVtzfsfdwRF+YccuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABApXMLx8582BHGEluoAZ21qcuxmrQnZJuPs409WXxjSAQICAAEMAgAAAADh9QUAAAAA', + ), + derivationPath: const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + sourceFingerprint: 123456789, + ), + origin: 'solflare', + dataType: CborSolSignDataType.transaction, + ); + + // Act + Uint8List actualSerializedCborBytes = actualCborSolSignRequest.toSerializedCbor(includeTagBool: false); + + // Assert + Uint8List expectedSerializedCborBytes = HexCodec.decode( + 'a501d825509b1deb4d3b7d4bad9bdd2b0d7b3dcb6d02589601000103c8d842a2f17fd7aab608ce2ea535a6e958dffa20caf669b347b911c4171965530f957620b228bae2b94c82ddd4c093983a67365555b737ec7ddc1117e61c72e0000000000000000000000000000000000000000000000000000000000000000010295cc2f1f39f3604718496ea00676d6a72ec66ad09d926e3ece34f565f18d201020200010c0200000000e1f5050000000003d90130a20188182cf51901f5f500f500f5021a075bcd150568736f6c666c6172650601', + ); + + expect(actualSerializedCborBytes, expectedSerializedCborBytes); + }); + }); + + group('Tests of CborSolSignRequest.toCborMap()', () { + test('Should [return CborMap] from CborSolSignRequest (WITH tag)', () { + // Arrange + CborSolSignRequest actualCborSolSignRequest = CborSolSignRequest( + requestId: base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), + signData: base64Decode( + 'AQABA8jYQqLxf9eqtgjOLqU1pulY3/ogyvZps0e5EcQXGWVTD5V2ILIouuK5TILd1MCTmDpnNlVVtzfsfdwRF+YccuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABApXMLx8582BHGEluoAZ21qcuxmrQnZJuPs409WXxjSAQICAAEMAgAAAADh9QUAAAAA'), + derivationPath: const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + sourceFingerprint: 123456789, + ), + origin: 'solflare', + dataType: CborSolSignDataType.transaction, + ); + + // Act + CborMap actualCborMap = actualCborSolSignRequest.toCborMap(includeTagBool: true); + + // Assert + CborMap expectedCborMap = CborMap( + { + const CborSmallInt(1): CborBytes(base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), tags: [CborSpecialTag.uuid.tag]), + const CborSmallInt(2): CborBytes(base64Decode( + 'AQABA8jYQqLxf9eqtgjOLqU1pulY3/ogyvZps0e5EcQXGWVTD5V2ILIouuK5TILd1MCTmDpnNlVVtzfsfdwRF+YccuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABApXMLx8582BHGEluoAZ21qcuxmrQnZJuPs409WXxjSAQICAAEMAgAAAADh9QUAAAAA', + )), + const CborSmallInt(3): const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + sourceFingerprint: 123456789, + ).toCborMap(includeTagBool: true), + const CborSmallInt(5): CborString('solflare'), + const CborSmallInt(6): CborSmallInt(CborSolSignDataType.transaction.cborIndex), + }, + ); + + expect(actualCborMap, expectedCborMap); + }); + + test('Should [return CborMap] from CborSolSignRequest (WITHOUT tag)', () { + // Arrange + CborSolSignRequest actualCborSolSignRequest = CborSolSignRequest( + requestId: base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), + signData: base64Decode( + 'AQABA8jYQqLxf9eqtgjOLqU1pulY3/ogyvZps0e5EcQXGWVTD5V2ILIouuK5TILd1MCTmDpnNlVVtzfsfdwRF+YccuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABApXMLx8582BHGEluoAZ21qcuxmrQnZJuPs409WXxjSAQICAAEMAgAAAADh9QUAAAAA'), + derivationPath: const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + sourceFingerprint: 123456789, + ), + origin: 'solflare', + dataType: CborSolSignDataType.transaction, + ); + + // Act + CborMap actualCborMap = actualCborSolSignRequest.toCborMap(includeTagBool: true); + + // Assert + CborMap expectedCborMap = CborMap( + { + const CborSmallInt(1): CborBytes(base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), tags: [CborSpecialTag.uuid.tag]), + const CborSmallInt(2): CborBytes(base64Decode( + 'AQABA8jYQqLxf9eqtgjOLqU1pulY3/ogyvZps0e5EcQXGWVTD5V2ILIouuK5TILd1MCTmDpnNlVVtzfsfdwRF+YccuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABApXMLx8582BHGEluoAZ21qcuxmrQnZJuPs409WXxjSAQICAAEMAgAAAADh9QUAAAAA', + )), + const CborSmallInt(3): const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + sourceFingerprint: 123456789, + ).toCborMap(includeTagBool: true), + const CborSmallInt(5): CborString('solflare'), + const CborSmallInt(6): CborSmallInt(CborSolSignDataType.transaction.cborIndex), + }, + ); + + expect(actualCborMap, expectedCborMap); + }); + }); + + group('CborSolSignRequest.getCborSpecialTag()', () { + test('Should [return CborSpecialTag.solSignRequest] from CborSolSignRequest', () { + // Arrange + CborSolSignRequest actualCborSolSignRequest = CborSolSignRequest( + requestId: base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), + signData: base64Decode( + 'AQABA8jYQqLxf9eqtgjOLqU1pulY3/ogyvZps0e5EcQXGWVTD5V2ILIouuK5TILd1MCTmDpnNlVVtzfsfdwRF+YccuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABApXMLx8582BHGEluoAZ21qcuxmrQnZJuPs409WXxjSAQICAAEMAgAAAADh9QUAAAAA'), + derivationPath: const CborCryptoKeypath( + components: [ + CborPathComponent(index: 44, hardened: true), + CborPathComponent(index: 501, hardened: true), + CborPathComponent(index: 0, hardened: true), + CborPathComponent(index: 0, hardened: true), + ], + sourceFingerprint: 123456789, + ), + origin: 'solflare', + dataType: CborSolSignDataType.transaction, + ); + + // Act + CborSpecialTag actualCborSpecialTag = actualCborSolSignRequest.getCborSpecialTag(); + + // Assert + CborSpecialTag expectedCborSpecialTag = CborSpecialTag.solSignRequest; + + expect(actualCborSpecialTag, expectedCborSpecialTag); + }); + }); +} diff --git a/test/unit/codecs/cbor/solana/cbor_sol_signature_test.dart b/test/unit/codecs/cbor/solana/cbor_sol_signature_test.dart new file mode 100644 index 0000000..70ba486 --- /dev/null +++ b/test/unit/codecs/cbor/solana/cbor_sol_signature_test.dart @@ -0,0 +1,196 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:cbor/cbor.dart'; +import 'package:codec_utils/src/codecs/cbor/cbor_special_tag.dart'; +import 'package:codec_utils/src/codecs/cbor/solana/cbor_sol_signature.dart'; +import 'package:codec_utils/src/codecs/hex/hex_codec.dart'; +import 'package:test/test.dart'; + +void main() { + group('Tests of CborSolSignature.fromSerializedCbor()', () { + test('Should [return CborSolSignature] from serialized CBOR bytes (WITH tag)', () { + // Arrange + Uint8List actualSerializedCborBytes = HexCodec.decode( + 'd9044ea201d825509b1deb4d3b7d4bad9bdd2b0d7b3dcb6d025840d4f0a7bcd95bba1fbb1051885054730e3f47064288575aacc102fbbf6a9a14daa066991e360d3e3406c20c00a40973eff37c7d641e5b351ec4a99bfe86f335f7', + ); + + // Act + CborSolSignature actualCborSolSignature = CborSolSignature.fromSerializedCbor(actualSerializedCborBytes); + + // Assert + CborSolSignature expectedCborSolSignature = CborSolSignature( + requestId: base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), + signature: base64Decode('1PCnvNlbuh+7EFGIUFRzDj9HBkKIV1qswQL7v2qaFNqgZpkeNg0+NAbCDACkCXPv83x9ZB5bNR7EqZv+hvM19w=='), + ); + + expect(actualCborSolSignature, expectedCborSolSignature); + }); + + test('Should [return CborSolSignature] from serialized CBOR bytes (WITHOUT tag)', () { + // Arrange + Uint8List actualSerializedCborBytes = HexCodec.decode( + 'a201d825509b1deb4d3b7d4bad9bdd2b0d7b3dcb6d025840d4f0a7bcd95bba1fbb1051885054730e3f47064288575aacc102fbbf6a9a14daa066991e360d3e3406c20c00a40973eff37c7d641e5b351ec4a99bfe86f335f7', + ); + + // Act + CborSolSignature actualCborSolSignature = CborSolSignature.fromSerializedCbor(actualSerializedCborBytes); + + // Assert + CborSolSignature expectedCborSolSignature = CborSolSignature( + requestId: base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), + signature: base64Decode('1PCnvNlbuh+7EFGIUFRzDj9HBkKIV1qswQL7v2qaFNqgZpkeNg0+NAbCDACkCXPv83x9ZB5bNR7EqZv+hvM19w=='), + ); + + expect(actualCborSolSignature, expectedCborSolSignature); + }); + }); + + group('Tests of CborSolSignature.fromCborMap()', () { + test('Should [return CborSolSignature] from CborMap (WITH tag)', () { + // Arrange + CborMap actualCborMap = CborMap( + { + const CborSmallInt(1): CborBytes(base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), tags: [CborSpecialTag.uuid.tag]), + const CborSmallInt(2): CborBytes(base64Decode('1PCnvNlbuh+7EFGIUFRzDj9HBkKIV1qswQL7v2qaFNqgZpkeNg0+NAbCDACkCXPv83x9ZB5bNR7EqZv+hvM19w==')), + }, + tags: [CborSpecialTag.solSignature.tag], + ); + + // Act + CborSolSignature actualCborSolSignature = CborSolSignature.fromCborMap(actualCborMap); + + // Assert + CborSolSignature expectedCborSolSignature = CborSolSignature( + requestId: base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), + signature: base64Decode('1PCnvNlbuh+7EFGIUFRzDj9HBkKIV1qswQL7v2qaFNqgZpkeNg0+NAbCDACkCXPv83x9ZB5bNR7EqZv+hvM19w=='), + ); + + expect(actualCborSolSignature, expectedCborSolSignature); + }); + + test('Should [return CborSolSignature] from CborMap (WITHOUT tag)', () { + // Arrange + CborMap actualCborMap = CborMap( + { + const CborSmallInt(1): CborBytes(base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), tags: [CborSpecialTag.uuid.tag]), + const CborSmallInt(2): CborBytes(base64Decode('1PCnvNlbuh+7EFGIUFRzDj9HBkKIV1qswQL7v2qaFNqgZpkeNg0+NAbCDACkCXPv83x9ZB5bNR7EqZv+hvM19w==')), + }, + tags: [], + ); + + // Act + CborSolSignature actualCborSolSignature = CborSolSignature.fromCborMap(actualCborMap); + + // Assert + CborSolSignature expectedCborSolSignature = CborSolSignature( + requestId: base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), + signature: base64Decode('1PCnvNlbuh+7EFGIUFRzDj9HBkKIV1qswQL7v2qaFNqgZpkeNg0+NAbCDACkCXPv83x9ZB5bNR7EqZv+hvM19w=='), + ); + + expect(actualCborSolSignature, expectedCborSolSignature); + }); + }); + + group('Tests of CborSolSignature.toSerializedCbor()', () { + test('Should [return serialized CBOR bytes] from CborSolSignature (WITH tag)', () { + // Arrange + CborSolSignature actualCborSolSignature = CborSolSignature( + requestId: base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), + signature: base64Decode('1PCnvNlbuh+7EFGIUFRzDj9HBkKIV1qswQL7v2qaFNqgZpkeNg0+NAbCDACkCXPv83x9ZB5bNR7EqZv+hvM19w=='), + ); + + // Act + Uint8List actualSerializedCborBytes = actualCborSolSignature.toSerializedCbor(includeTagBool: true); + + // Assert + Uint8List expectedSerializedCborBytes = HexCodec.decode( + 'd9044ea201d825509b1deb4d3b7d4bad9bdd2b0d7b3dcb6d025840d4f0a7bcd95bba1fbb1051885054730e3f47064288575aacc102fbbf6a9a14daa066991e360d3e3406c20c00a40973eff37c7d641e5b351ec4a99bfe86f335f7', + ); + + expect(actualSerializedCborBytes, expectedSerializedCborBytes); + }); + + test('Should [return serialized CBOR bytes] from CborSolSignature (WITHOUT tag)', () { + // Arrange + CborSolSignature actualCborSolSignature = CborSolSignature( + requestId: base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), + signature: base64Decode('1PCnvNlbuh+7EFGIUFRzDj9HBkKIV1qswQL7v2qaFNqgZpkeNg0+NAbCDACkCXPv83x9ZB5bNR7EqZv+hvM19w=='), + ); + + // Act + Uint8List actualSerializedCborBytes = actualCborSolSignature.toSerializedCbor(includeTagBool: false); + + // Assert + Uint8List expectedSerializedCborBytes = HexCodec.decode( + 'a201d825509b1deb4d3b7d4bad9bdd2b0d7b3dcb6d025840d4f0a7bcd95bba1fbb1051885054730e3f47064288575aacc102fbbf6a9a14daa066991e360d3e3406c20c00a40973eff37c7d641e5b351ec4a99bfe86f335f7', + ); + + expect(actualSerializedCborBytes, expectedSerializedCborBytes); + }); + }); + + group('Tests of CborSolSignature.toCborMap()', () { + test('Should [return CborMap] from CborSolSignature (WITH tag)', () { + // Arrange + CborSolSignature actualCborSolSignature = CborSolSignature( + requestId: base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), + signature: base64Decode('1PCnvNlbuh+7EFGIUFRzDj9HBkKIV1qswQL7v2qaFNqgZpkeNg0+NAbCDACkCXPv83x9ZB5bNR7EqZv+hvM19w=='), + ); + + // Act + CborMap actualCborMap = actualCborSolSignature.toCborMap(includeTagBool: true); + + // Assert + CborMap expectedCborMap = CborMap( + { + const CborSmallInt(1): CborBytes(base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), tags: [CborSpecialTag.uuid.tag]), + const CborSmallInt(2): CborBytes(base64Decode('1PCnvNlbuh+7EFGIUFRzDj9HBkKIV1qswQL7v2qaFNqgZpkeNg0+NAbCDACkCXPv83x9ZB5bNR7EqZv+hvM19w==')), + }, + tags: [CborSpecialTag.solSignature.tag], + ); + + expect(actualCborMap, expectedCborMap); + }); + + test('Should [return CborMap] from CborSolSignature (WITHOUT tag)', () { + // Arrange + CborSolSignature actualCborSolSignature = CborSolSignature( + requestId: base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), + signature: base64Decode('1PCnvNlbuh+7EFGIUFRzDj9HBkKIV1qswQL7v2qaFNqgZpkeNg0+NAbCDACkCXPv83x9ZB5bNR7EqZv+hvM19w=='), + ); + + // Act + CborMap actualCborMap = actualCborSolSignature.toCborMap(includeTagBool: false); + + // Assert + CborMap expectedCborMap = CborMap( + { + const CborSmallInt(1): CborBytes(base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), tags: [CborSpecialTag.uuid.tag]), + const CborSmallInt(2): CborBytes(base64Decode('1PCnvNlbuh+7EFGIUFRzDj9HBkKIV1qswQL7v2qaFNqgZpkeNg0+NAbCDACkCXPv83x9ZB5bNR7EqZv+hvM19w==')), + }, + tags: [CborSpecialTag.solSignature.tag], + ); + + expect(actualCborMap, expectedCborMap); + }); + }); + + group('Tests of CborSolSignature.getCborSpecialTag()', () { + test('Should [return CborSpecialTag.solSignature] from CborSolSignature', () { + // Arrange + CborSolSignature actualCborSolSignature = CborSolSignature( + requestId: base64Decode('mx3rTTt9S62b3SsNez3LbQ=='), + signature: base64Decode('1PCnvNlbuh+7EFGIUFRzDj9HBkKIV1qswQL7v2qaFNqgZpkeNg0+NAbCDACkCXPv83x9ZB5bNR7EqZv+hvM19w=='), + ); + + // Act + CborSpecialTag actualCborSpecialTag = actualCborSolSignature.getCborSpecialTag(); + + // Assert + CborSpecialTag expectedCborSpecialTag = CborSpecialTag.solSignature; + + expect(actualCborSpecialTag, expectedCborSpecialTag); + }); + }); +} diff --git a/test/unit/codecs/compact_u16/compact_u16_decoder_test.dart b/test/unit/codecs/compact_u16/compact_u16_decoder_test.dart new file mode 100644 index 0000000..da7615d --- /dev/null +++ b/test/unit/codecs/compact_u16/compact_u16_decoder_test.dart @@ -0,0 +1,126 @@ +import 'dart:typed_data'; +import 'package:codec_utils/src/codecs/byte_reader/byte_reader.dart'; +import 'package:codec_utils/src/codecs/compact_u16/compact_u16_decoder.dart'; +import 'package:test/test.dart'; + +void main() { + group('Tests of CompactU16Decoder.decode()', () { + test('Should [return 0] for input 0x00 (MIN one-byte)', () { + // Arrange + ByteReader actualByteReader = ByteReader(Uint8List.fromList([0x00])); + + // Act + int actualDecodedValue = CompactU16Decoder.decode(actualByteReader); + int actualOffset = actualByteReader.offset; + + // Assert + int expectedDecodedValue = 0; + int expectedOffset = 1; + + expect(actualDecodedValue, expectedDecodedValue); + expect(actualOffset, expectedOffset); + }); + + test('Should [return 127] for input 0x7F (MAX one-byte)', () { + // Arrange + ByteReader actualByteReader = ByteReader(Uint8List.fromList([0x7F])); + + // Act + int actualDecodedValue = CompactU16Decoder.decode(actualByteReader); + int actualOffset = actualByteReader.offset; + + // Assert + int expectedDecodedValue = 127; + int expectedOffset = 1; + + expect(actualDecodedValue, expectedDecodedValue); + expect(actualOffset, expectedOffset); + }); + + test('Should [return 128] for input 0x80 0x01 (MIN two-byte)', () { + // Arrange + ByteReader actualByteReader = ByteReader(Uint8List.fromList([0x80, 0x01])); + + // Act + int actualDecodedValue = CompactU16Decoder.decode(actualByteReader); + int actualOffset = actualByteReader.offset; + + // Assert + int expectedDecodedValue = 128; + int expectedOffset = 2; + + expect(actualDecodedValue, expectedDecodedValue); + expect(actualOffset, expectedOffset); + }); + + test('Should [return 16383] for input 0xFF 0x7F (MAX two-byte)', () { + // Arrange + ByteReader actualByteReader = ByteReader(Uint8List.fromList([0xFF, 0x7F])); + + // Act + int actualDecodedValue = CompactU16Decoder.decode(actualByteReader); + int actualOffset = actualByteReader.offset; + + // Assert + int expectedDecodedValue = 16383; + int expectedOffset = 2; + + expect(actualDecodedValue, expectedDecodedValue); + expect(actualOffset, expectedOffset); + }); + + test('Should [return 16384] for input 0x80 0x80 0x01 (MIN three-byte)', () { + // Arrange + ByteReader actualByteReader = ByteReader(Uint8List.fromList([0x80, 0x80, 0x01])); + + // Act + int actualDecodedValue = CompactU16Decoder.decode(actualByteReader); + int actualOffset = actualByteReader.offset; + + // Assert + int expectedDecodedValue = 16384; + int expectedOffset = 3; + + expect(actualDecodedValue, expectedDecodedValue); + expect(actualOffset, expectedOffset); + }); + + test('Should [return 65535] for input 0xFF 0xFF 0x03 (MAX three-byte)', () { + // Arrange + ByteReader actualByteReader = ByteReader(Uint8List.fromList([0xFF, 0xFF, 0x03])); + + // Act + int actualDecodedValue = CompactU16Decoder.decode(actualByteReader); + int actualOffset = actualByteReader.offset; + + // Assert + int expectedDecodedValue = 65535; + int expectedOffset = 3; + + expect(actualDecodedValue, expectedDecodedValue); + expect(actualOffset, expectedOffset); + }); + + test('Should [throw Exception] when a byte has a continuation bit set but the next byte is 0', () { + // Arrange + ByteReader actualByteReader = ByteReader(Uint8List.fromList([0xFF, 0x0])); + + // Assert + int expectedOffset = 0; + + expect(() => CompactU16Decoder.decode(actualByteReader), throwsException); + expect(actualByteReader.offset, expectedOffset); + }); + + test('Should [throw Exception] when attempting to read past the third byte', () { + // Arrange + ByteReader actualByteReader = ByteReader(Uint8List.fromList([0xFF, 0xFF, 0xFF, 0xFF])); + + // Assert + int expectedOffset = 0; + + expect(() => CompactU16Decoder.decode(actualByteReader), throwsException); + expect(actualByteReader.offset, expectedOffset); + }); + }); +}