From 6c06899418a7928c32ce9ce1529d684f8210f16c Mon Sep 17 00:00:00 2001 From: Kamil Sobonski <39964811+SucharMistrz@users.noreply.github.com> Date: Wed, 7 May 2025 11:21:32 +0200 Subject: [PATCH] Feature: Solana cbor and compact-u16 decoding This branch introduces components required for connecting Solana wallets through Keystone, signing Solana transactions as well as decoding variable-length integers in Solana. List of changes: - updated a_cbor_tagged_object.dart and cbor_special_tag.dart to include new Solana elements - created cbor_crypto_multi_accounts.dart, used to connect Solana wallets - created cbor_sol_sign_request.dart and cbor_sol_signature.dart, used to request and return Solana signature - created compact_u16_decoder.dart, used for decoding variable-length integers in Solana - created byte_reader.dart, a helper used for sequential reading of binary data - unrelated with domain: updated base58_codec.dart to fix an issue where the decode() method applied zero-byte padding on top of the already decoded value when the input consisted entirely of '1' Base58 characters. For example, decoding 32 zeros returned the decoded value of 0 and padding added 32 zeros on top of it, returning 33 zeros in total, instead of 32. It caused the Solana System Program address to be decoded incorrectly. Solana System Program is the most important program of all, responsible for: new account creation, space allocation, assigning program ownership and transferring SOL. --- lib/codec_utils.dart | 117 +++-- lib/src/codecs/base/base58_codec.dart | 5 +- lib/src/codecs/byte_reader/byte_reader.dart | 32 ++ lib/src/codecs/cbor/a_cbor_tagged_object.dart | 6 + lib/src/codecs/cbor/cbor_special_tag.dart | 7 +- .../crypto/cbor_crypto_multi_accounts.dart | 85 ++++ lib/src/codecs/cbor/export.dart | 4 + .../cbor/solana/cbor_sol_sign_request.dart | 98 ++++ .../cbor/solana/cbor_sol_signature.dart | 61 +++ .../metadata/cbor_sol_sign_data_type.dart | 14 + .../compact_u16/compact_u16_decoder.dart | 40 ++ pubspec.yaml | 2 +- test/unit/codecs/base/base58_codec_test.dart | 21 + .../codecs/byte_reader/byte_reader_test.dart | 91 ++++ .../cbor_crypto_multi_accounts_test.dart | 440 ++++++++++++++++++ .../solana/cbor_sol_sign_request_test.dart | 358 ++++++++++++++ .../cbor/solana/cbor_sol_signature_test.dart | 196 ++++++++ .../compact_u16/compact_u16_decoder_test.dart | 126 +++++ 18 files changed, 1651 insertions(+), 52 deletions(-) create mode 100644 lib/src/codecs/byte_reader/byte_reader.dart create mode 100644 lib/src/codecs/cbor/crypto/cbor_crypto_multi_accounts.dart create mode 100644 lib/src/codecs/cbor/solana/cbor_sol_sign_request.dart create mode 100644 lib/src/codecs/cbor/solana/cbor_sol_signature.dart create mode 100644 lib/src/codecs/cbor/solana/metadata/cbor_sol_sign_data_type.dart create mode 100644 lib/src/codecs/compact_u16/compact_u16_decoder.dart create mode 100644 test/unit/codecs/byte_reader/byte_reader_test.dart create mode 100644 test/unit/codecs/cbor/crypto/cbor_crypto_multi_accounts_test.dart create mode 100644 test/unit/codecs/cbor/solana/cbor_sol_sign_request_test.dart create mode 100644 test/unit/codecs/cbor/solana/cbor_sol_signature_test.dart create mode 100644 test/unit/codecs/compact_u16/compact_u16_decoder_test.dart 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); + }); + }); +}