diff --git a/.github/workflows/version_and_tests.yaml b/.github/workflows/version_and_tests.yaml index 8bce557..762918b 100644 --- a/.github/workflows/version_and_tests.yaml +++ b/.github/workflows/version_and_tests.yaml @@ -8,7 +8,7 @@ on: jobs: app-version-check: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: # https://github.com/marketplace/actions/checkout - uses: actions/checkout@main @@ -52,7 +52,7 @@ jobs: exit 1 run-unit-tests: needs: app-version-check - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: # https://github.com/marketplace/actions/checkout - uses: actions/checkout@main diff --git a/lib/codec_utils.dart b/lib/codec_utils.dart index 61dd714..f72f7e5 100644 --- a/lib/codec_utils.dart +++ b/lib/codec_utils.dart @@ -11,12 +11,18 @@ export 'src/codecs/base/base58_codec.dart'; /// Classes designed for encoding data using the Bech32 encoding scheme. /// Usage: -/// ``` -/// String encodedBech32 = Bech32Codec.encode(Bech32Pair(hrp: 'crypto', data: base64Decode('KxmiVli7oFEs8N5rjnzLtw7eym0='))); -/// Bech32Pair decodedBech32 = Bech32Codec.decode("crypto19vv6y4jchws9zt8sme4culxtku8dajndgyhdm2"); +/// `` +/// List convertedUint5List = BytesUtils.convertBits(Bech32.uint8List, 8, 5, padBool: true); +/// +/// Bech32 bech32 = Bech32.fromUint5List('bc', convertedUint5List) +/// String encodedBech32 = Bech32Encoder().encode(bech32); +/// +/// Bech32 decodedBech32 = Bech32Codec().decode("crypto19vv6y4jchws9zt8sme4culxtku8dajndgyhdm2"); +/// +/// SegWit segWit = SegWit(hrp, witnessVersion, witnessProgramUint8List); +/// String encodedSegWit SegWitEncoder().encode(segWit); /// -/// String encodedSegwit = SegwitBech32Codec.encode('bc', 0, base64Decode('KxmiVli7oFEs8N5rjnzLtw7eym0=')); -/// Uint8List decodedSegwit = SegwitBech32Codec.decode("bc1q9vv6y4jchws9zt8sme4culxtku8dajnd5jq660"); +/// SegWit decodedSegWit = SegWitDecoder().decode("bc1q9vv6y4jchws9zt8sme4culxtku8dajnd5jq660"); /// ``` export 'src/codecs/bech32/export.dart'; diff --git a/lib/src/codecs/bech32/bech32.dart b/lib/src/codecs/bech32/bech32.dart new file mode 100644 index 0000000..0ecc643 --- /dev/null +++ b/lib/src/codecs/bech32/bech32.dart @@ -0,0 +1,31 @@ +import 'dart:typed_data'; + +import 'package:codec_utils/src/utils/bytes_utils.dart'; +import 'package:equatable/equatable.dart'; + +/// Class representing a Bech32 encoded pair +/// +/// A Bech32 string consists of two main parts: +/// - The human-readable part (HRP), which indicates the network or context. +/// - The data payload encoded using a 5-bit scheme. +class Bech32 extends Equatable { + /// The human-readable part (hrp) of the Bech32 encoded pair. + final String hrp; + + /// The data part of the Bech32 encoded pair. + final Uint8List uint8List; + + const Bech32(this.hrp, this.uint8List); + + factory Bech32.fromUint5List(String hrp, List uint5List) { + Uint8List uint8List = BytesUtils.convertBits(uint5List, 5, 8); + return Bech32(hrp, uint8List); + } + + List get uint5List { + return BytesUtils.convertBits(uint8List, 8, 5, allowPaddingBool: false); + } + + @override + List get props => [hrp, uint8List]; +} diff --git a/lib/src/codecs/bech32/bech32_codec.dart b/lib/src/codecs/bech32/bech32_codec.dart deleted file mode 100644 index b7750c2..0000000 --- a/lib/src/codecs/bech32/bech32_codec.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'dart:typed_data'; - -import 'package:bech32/bech32.dart'; -import 'package:codec_utils/src/codecs/bech32/bech32_pair.dart'; - -/// The [Bech32Codec] class is designed for encoding data using the Bech32 encoding scheme. -/// The specification for Bech32 encoding can be found in BIP-0173 and BIP-0350: -/// https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki -/// https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki -class Bech32Codec { - static String encode(Bech32Pair bech32pair) { - Uint8List convertedData = _convertBits(bech32pair.data, 8, 5); - // TODO(dominik): Implement custom Bech32 encoding - return bech32.encode(Bech32(bech32pair.hrp, convertedData)); - } - - static Bech32Pair decode(String bechAddress) { - Bech32 decodedBech32 = bech32.decode(bechAddress); - // TODO(dominik): Implement custom Bech32 decoding - Uint8List convertedData = _convertBits(decodedBech32.data, 5, 8, padBool: false); - - return Bech32Pair(data: convertedData, hrp: decodedBech32.hrp); - } - - static Uint8List _convertBits( - List data, - int startBitIndex, - int endBitIndex, { - bool padBool = true, - }) { - int acc = 0; - int bits = 0; - List result = []; - int maxV = (1 << endBitIndex) - 1; - - for (int v in data) { - if (v < 0 || (v >> startBitIndex) != 0) { - throw Exception('Got address byte smaller than zero or greater than 2^startBitIndex'); - } - acc = (acc << startBitIndex) | v; - bits += startBitIndex; - while (bits >= endBitIndex) { - bits -= endBitIndex; - result.add((acc >> bits) & maxV); - } - } - - if (padBool) { - if (bits > 0) { - result.add((acc << (endBitIndex - bits)) & maxV); - } - } else if (bits >= startBitIndex) { - throw Exception('Illegal zero padding'); - } else if (((acc << (endBitIndex - bits)) & maxV) != 0) { - throw Exception('Non zero'); - } - - return Uint8List.fromList(result); - } -} diff --git a/lib/src/codecs/bech32/bech32_constants.dart b/lib/src/codecs/bech32/bech32_constants.dart new file mode 100644 index 0000000..4ac02c0 --- /dev/null +++ b/lib/src/codecs/bech32/bech32_constants.dart @@ -0,0 +1,17 @@ +class Bech32Constants { + static const int maxInputLength = 90; + static const int checksumLength = 6; + static const String separator = '1'; + static const List charList = [ + 'q', 'p', 'z', 'r', 'y', '9', 'x', // + '8', 'g', 'f', '2', 't', 'v', 'd', + 'w', '0', 's', '3', 'j', 'n', '5', + '4', 'k', 'h', 'c', 'e', '6', 'm', + 'u', 'a', '7', 'l', + ]; + + static const List generatorList = [ + 0x3b6a57b2, 0x26508e6d, 0x1ea119fa, // + 0x3d4233dd, 0x2a1462b3, + ]; +} diff --git a/lib/src/codecs/bech32/bech32_decoder.dart b/lib/src/codecs/bech32/bech32_decoder.dart new file mode 100644 index 0000000..693606c --- /dev/null +++ b/lib/src/codecs/bech32/bech32_decoder.dart @@ -0,0 +1,83 @@ +// This class was primarily influenced by: +// Copyright 2020 Harm Aarts +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +import 'package:codec_utils/codec_utils.dart'; +import 'package:codec_utils/src/codecs/bech32/bech32_constants.dart'; +import 'package:codec_utils/src/codecs/bech32/bech32_utils.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_bech32_exception.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_checksum_exception.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_hrp_exception.dart'; + +/// A utility class for decoding Bech32-encoded strings into [Bech32] objects. +/// +/// This decoder validates and parses a Bech32 string into its human-readable part (HRP) +/// and data payload, following the specifications described in BIP 173 and BIP 350. +class Bech32Decoder { + Bech32 decode(String bechAddress, [int maxInputLength = Bech32Constants.maxInputLength]) { + if (bechAddress.length > maxInputLength) { + throw InvalidBech32Exception('Bech32 is too long: ${bechAddress.length} > 90'); + } + if (bechAddress.toLowerCase() != bechAddress && bechAddress.toUpperCase() != bechAddress) { + throw InvalidBech32Exception('Bech32 input must be either all lowercase or all uppercase: $bechAddress'); + } + if (bechAddress.lastIndexOf(Bech32Constants.separator) == -1) { + throw InvalidBech32Exception('Bech32 string is missing the required separator between HRP and data'); + } + + int separatorPosition = bechAddress.lastIndexOf(Bech32Constants.separator); + + if (bechAddress.length - separatorPosition - 1 - Bech32Constants.checksumLength < 0) { + throw InvalidChecksumException('Checksum is too short: ${bechAddress.length} < 6'); + } + + if (separatorPosition == 0) { + throw InvalidHrpException('HRP is too short: $separatorPosition'); + } + + String normalizedInput = bechAddress.toLowerCase(); + + String hrp = normalizedInput.substring(0, separatorPosition); + String data = normalizedInput.substring(separatorPosition + 1, bechAddress.length - Bech32Constants.checksumLength); + String checksum = normalizedInput.substring(bechAddress.length - Bech32Constants.checksumLength); + + if (hrp.codeUnits.any((int element) => element < 33 || element > 126)) { + throw InvalidHrpException('HRP contains invalid characters: $hrp'); + } + + List uint5List = data.split('').map((String element) { + return Bech32Constants.charList.indexOf(element); + }).toList(); + + if (uint5List.any((int element) => element == -1)) { + throw InvalidBech32Exception('Bech32 has undefined character: ${data[uint5List.indexOf(-1)]}'); + } + + List checksumUint5List = checksum.split('').map((String element) { + return Bech32Constants.charList.indexOf(element); + }).toList(); + + if (checksumUint5List.any((int element) => element == -1)) { + int invalidIndex = checksumUint5List.indexOf(-1); + throw InvalidChecksumException('Checksum contains undefined character at position: ${checksumUint5List[invalidIndex]}'); + } + + List expandedHrpUint5List = Bech32Utils.expandHrp(hrp); + + if (Bech32Utils.calculatePolymodChecksum(expandedHrpUint5List + (uint5List + checksumUint5List)) != 1) { + throw InvalidChecksumException('Checksum verification failed for input: $bechAddress'); + } + + return Bech32.fromUint5List(hrp, uint5List); + } +} diff --git a/lib/src/codecs/bech32/bech32_encoder.dart b/lib/src/codecs/bech32/bech32_encoder.dart new file mode 100644 index 0000000..68461e1 --- /dev/null +++ b/lib/src/codecs/bech32/bech32_encoder.dart @@ -0,0 +1,62 @@ +// This class was primarily influenced by: +// Copyright 2020 Harm Aarts +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +import 'dart:typed_data'; + +import 'package:codec_utils/codec_utils.dart'; +import 'package:codec_utils/src/codecs/bech32/bech32_constants.dart'; +import 'package:codec_utils/src/codecs/bech32/bech32_utils.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_bech32_exception.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_hrp_exception.dart'; + +/// A utility class for encoding [Bech32] objects into valid Bech32 strings. +/// +/// This encoder converts a [Bech32] instance (which contains a human-readable part (HRP) +/// and a data payload) into a properly formatted Bech32 string, as specified in +/// BIP 173 and BIP 350. +class Bech32Encoder { + /// Encodes a [Bech32] object [bech32] into a valid Bech32 string. + String encode(Bech32 bech32, [int maxLength = Bech32Constants.maxInputLength]) { + String hrp = bech32.hrp.toLowerCase(); + List uint5List = bech32.uint5List; + int length = hrp.length + uint5List.length + Bech32Constants.separator.length + Bech32Constants.checksumLength; + + if (length > maxLength) { + throw InvalidBech32Exception('Bech32 is too long: ${hrp.length + uint5List.length + 1 + Bech32Constants.checksumLength}'); + } + + if (hrp.isEmpty) { + throw InvalidHrpException('HRP is empty'); + } + + List checkSummedListUint5List = uint5List + _createChecksum(hrp, uint5List); + + return hrp + Bech32Constants.separator + checkSummedListUint5List.map((int element) => Bech32Constants.charList[element]).join(); + } + + /// Computes a 6-byte checksum for the given [hrp] and [uint5List], + /// following Bech32 checksum rules (BIP 173). + /// + /// Returns a [Uint8List] representing the 6 checksum bytes. + List _createChecksum(String hrp, List uint5List) { + List payloadUint5List = Bech32Utils.expandHrp(hrp) + uint5List + [0, 0, 0, 0, 0, 0]; + int polymodValue = Bech32Utils.calculatePolymodChecksum(payloadUint5List) ^ 1; + + List checksumUint5List = [0, 0, 0, 0, 0, 0]; + + for (int i = 0; i < checksumUint5List.length; i++) { + checksumUint5List[i] = (polymodValue >> (5 * (5 - i))) & 31; + } + return checksumUint5List; + } +} diff --git a/lib/src/codecs/bech32/bech32_pair.dart b/lib/src/codecs/bech32/bech32_pair.dart deleted file mode 100644 index b241191..0000000 --- a/lib/src/codecs/bech32/bech32_pair.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'dart:typed_data'; - -import 'package:equatable/equatable.dart'; - -/// Class representing a Bech32 encoded pair -class Bech32Pair extends Equatable { - /// The human-readable part (hrp) of the Bech32 encoded pair. - final String hrp; - - /// The data part of the Bech32 encoded pair. - final Uint8List data; - - /// Creates a [Bech32Pair] with the given [hrp] and [data]. - const Bech32Pair({ - required this.hrp, - required this.data, - }); - - @override - List get props => [hrp, data]; -} diff --git a/lib/src/codecs/bech32/bech32_utils.dart b/lib/src/codecs/bech32/bech32_utils.dart new file mode 100644 index 0000000..7269991 --- /dev/null +++ b/lib/src/codecs/bech32/bech32_utils.dart @@ -0,0 +1,31 @@ +import 'package:codec_utils/src/codecs/bech32/bech32_constants.dart'; + +class Bech32Utils { + /// Calculates the polymod checksum value for a list of integers [payloadUint5List]. + static int calculatePolymodChecksum(List payloadUint5List) { + int checksum = 1; + for (int element in payloadUint5List) { + int highBit = checksum >> 25; + checksum = (checksum & 0x1ffffff) << 5 ^ element; + for (int i = 0; i < Bech32Constants.generatorList.length; i++) { + if ((highBit >> i) & 1 == 1) { + checksum ^= Bech32Constants.generatorList[i]; + } + } + } + return checksum; + } + + /// Expands the human-readable part (HRP) string [hrp] into a list of integers. + /// + /// This transformation is required for checksum calculation as defined + /// in BIP 173. The HRP is split into higher and lower 5-bit groups, separated by a zero delimiter. + /// + /// Returns a [resultUint5List] representing the expanded HRP. + static List expandHrp(String hrp) { + List resultUint5List = hrp.codeUnits.map((int element) => element >> 5).toList(); + resultUint5List = resultUint5List + [0]; + resultUint5List = resultUint5List + hrp.codeUnits.map((int element) => element & 31).toList(); + return resultUint5List; + } +} diff --git a/lib/src/codecs/bech32/exceptions/invalid_bech32_exception.dart b/lib/src/codecs/bech32/exceptions/invalid_bech32_exception.dart new file mode 100644 index 0000000..beed847 --- /dev/null +++ b/lib/src/codecs/bech32/exceptions/invalid_bech32_exception.dart @@ -0,0 +1,8 @@ +class InvalidBech32Exception implements Exception { + final String? message; + + InvalidBech32Exception(this.message); + + @override + String toString() => message ?? runtimeType.toString(); +} diff --git a/lib/src/codecs/bech32/exceptions/invalid_checksum_exception.dart b/lib/src/codecs/bech32/exceptions/invalid_checksum_exception.dart new file mode 100644 index 0000000..81f4976 --- /dev/null +++ b/lib/src/codecs/bech32/exceptions/invalid_checksum_exception.dart @@ -0,0 +1,8 @@ +class InvalidChecksumException implements Exception { + final String? message; + + InvalidChecksumException(this.message); + + @override + String toString() => message ?? runtimeType.toString(); +} diff --git a/lib/src/codecs/bech32/exceptions/invalid_hrp_exception.dart b/lib/src/codecs/bech32/exceptions/invalid_hrp_exception.dart new file mode 100644 index 0000000..acf8b3b --- /dev/null +++ b/lib/src/codecs/bech32/exceptions/invalid_hrp_exception.dart @@ -0,0 +1,8 @@ +class InvalidHrpException implements Exception { + final String? message; + + InvalidHrpException(this.message); + + @override + String toString() => message ?? runtimeType.toString(); +} diff --git a/lib/src/codecs/bech32/exceptions/invalid_witness_program_exception.dart b/lib/src/codecs/bech32/exceptions/invalid_witness_program_exception.dart new file mode 100644 index 0000000..ceab659 --- /dev/null +++ b/lib/src/codecs/bech32/exceptions/invalid_witness_program_exception.dart @@ -0,0 +1,8 @@ +class InvalidWitnessProgramException implements Exception { + final String? message; + + InvalidWitnessProgramException(this.message); + + @override + String toString() => message ?? runtimeType.toString(); +} diff --git a/lib/src/codecs/bech32/exceptions/invalid_witness_version_exception.dart b/lib/src/codecs/bech32/exceptions/invalid_witness_version_exception.dart new file mode 100644 index 0000000..0f0c97c --- /dev/null +++ b/lib/src/codecs/bech32/exceptions/invalid_witness_version_exception.dart @@ -0,0 +1,8 @@ +class InvalidWitnessVersionException implements Exception { + final String? message; + + InvalidWitnessVersionException(this.message); + + @override + String toString() => message ?? runtimeType.toString(); +} diff --git a/lib/src/codecs/bech32/export.dart b/lib/src/codecs/bech32/export.dart index cb7490a..48c1c07 100644 --- a/lib/src/codecs/bech32/export.dart +++ b/lib/src/codecs/bech32/export.dart @@ -1,3 +1,6 @@ -export 'bech32_codec.dart'; -export 'bech32_pair.dart'; -export 'segwit_bech32_codec.dart'; +export 'bech32.dart'; +export 'bech32_decoder.dart'; +export 'bech32_encoder.dart'; +export 'seg_wit/seg_wit.dart'; +export 'seg_wit/seg_wit_decoder.dart'; +export 'seg_wit/seg_wit_encoder.dart'; diff --git a/lib/src/codecs/bech32/seg_wit/seg_wit.dart b/lib/src/codecs/bech32/seg_wit/seg_wit.dart new file mode 100644 index 0000000..246e9cd --- /dev/null +++ b/lib/src/codecs/bech32/seg_wit/seg_wit.dart @@ -0,0 +1,25 @@ +import 'dart:typed_data'; + +import 'package:equatable/equatable.dart'; + +/// A class representing a Segregated Witness (SegWit) address. +/// +/// This class holds the human-readable part (HRP), witness version, +/// and witness program list as defined in BIP 173. +/// +/// You can use this class to represent or validate SegWit addresses in Bech32 encoding. +class SegWit extends Equatable { + /// The human-readable part ("bc" for Bitcoin mainnet). + final String hrp; + + /// The witness version (0–16). + final int witnessVersion; + + /// The witness program data as a list of integers. + final Uint8List witnessProgramUint8List; + + const SegWit(this.hrp, this.witnessVersion, this.witnessProgramUint8List); + + @override + List get props => [hrp, witnessVersion, witnessProgramUint8List]; +} diff --git a/lib/src/codecs/bech32/seg_wit/seg_wit_decoder.dart b/lib/src/codecs/bech32/seg_wit/seg_wit_decoder.dart new file mode 100644 index 0000000..551990c --- /dev/null +++ b/lib/src/codecs/bech32/seg_wit/seg_wit_decoder.dart @@ -0,0 +1,70 @@ +// This class was primarily influenced by: +// Copyright 2020 Harm Aarts +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +import 'dart:typed_data'; + +import 'package:codec_utils/codec_utils.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_hrp_exception.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_witness_program_exception.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_witness_version_exception.dart'; +import 'package:codec_utils/src/utils/bytes_utils.dart'; + +/// A utility class for decoding Segregated Witness (SegWit) addresses encoded in Bech32 format. +/// +/// The [SegWitDecoder] class provides method to parse a Bech32-encoded string +/// and extract its SegWit-specific fields: human-readable part (HRP), witness version, +/// and witness program (programList). +/// +/// The decoder performs steps to ensure the address conforms to +/// BIP 173 and BIP 350 specifications: +/// +/// - Verifies that the HRP is supported "bc" for mainnet, "tb" for testnet). +/// - Checks that the witness version is within the valid range (0–16). +/// - Converts the data part from 5-bit groups (Bech32) to 8-bit groups (program data). +/// - Validates that the witness program has an allowed length and structure depending on its version. +/// +class SegWitDecoder { + SegWit decode(String input) { + Bech32 decodedBech32 = Bech32Decoder().decode(input); + + List convertedUint5List = BytesUtils.convertBits(decodedBech32.uint8List, 8, 5); + + if (decodedBech32.hrp != 'bc' && decodedBech32.hrp != 'tb') { + throw InvalidHrpException('Invalid hrp ${decodedBech32.hrp}'); + } + if (convertedUint5List.isEmpty) { + throw const FormatException('Empty data'); + } + + int witnessVersion = convertedUint5List[0]; + + if (witnessVersion > 16) { + throw InvalidWitnessVersionException('Invalid witness version $witnessVersion'); + } + + Uint8List witnessProgramUint8List = BytesUtils.convertBits(convertedUint5List.sublist(1), 5, 8, allowPaddingBool: false); + + if (witnessProgramUint8List.length < 2) { + throw InvalidWitnessProgramException('Too short program'); + } + if (witnessProgramUint8List.length > 40) { + throw InvalidWitnessProgramException('Too long program'); + } + if (witnessVersion == 0 && (witnessProgramUint8List.length != 20 && witnessProgramUint8List.length != 32)) { + throw InvalidWitnessProgramException('Invalid version $witnessVersion and invalid program length ${witnessProgramUint8List.length}'); + } + + return SegWit(decodedBech32.hrp, witnessVersion, witnessProgramUint8List); + } +} diff --git a/lib/src/codecs/bech32/seg_wit/seg_wit_encoder.dart b/lib/src/codecs/bech32/seg_wit/seg_wit_encoder.dart new file mode 100644 index 0000000..b26720d --- /dev/null +++ b/lib/src/codecs/bech32/seg_wit/seg_wit_encoder.dart @@ -0,0 +1,64 @@ +// This class was primarily influenced by: +// Copyright 2020 Harm Aarts +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +import 'dart:typed_data'; + +import 'package:codec_utils/codec_utils.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_witness_program_exception.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_witness_version_exception.dart'; +import 'package:codec_utils/src/utils/bytes_utils.dart'; + +/// A utility class for encoding Segregated Witness (SegWit) data +/// into a Bech32-encoded address string. +/// +/// This encoder takes a [SegWit] object and converts it into a valid +/// Bech32 address string following BIP 173 and BIP 350 specifications. +/// +/// ## Validation steps +/// +/// Before encoding, the encoder validates: +/// - The witness version is within the valid range (0–16). +/// - The witness program is neither too short nor too long. +/// - The witness program length is compatible with the specified version. +/// +/// ## Encoding process +/// +/// - Converts the witness program from 8-bit bytes to 5-bit groups (Bech32 data part). +/// - Prefixes the data list with the witness version. +/// - Encodes the final data list together with the human-readable part (HRP) +/// into a Bech32 address string. +class SegWitEncoder { + String encode(SegWit segWit) { + int witnessVersion = segWit.witnessVersion; + Uint8List witnessProgramUint8List = segWit.witnessProgramUint8List; + if (witnessVersion > 16) { + throw InvalidWitnessVersionException('Witness version is invalid $witnessVersion'); + } + if (witnessProgramUint8List.length < 2) { + throw InvalidWitnessProgramException('Witness program is too short ${witnessProgramUint8List.length}'); + } + if (witnessProgramUint8List.length > 40) { + throw InvalidWitnessProgramException('Witness program is too long ${witnessProgramUint8List.length}'); + } + if (witnessVersion == 0 && (witnessProgramUint8List.length != 20 && witnessProgramUint8List.length != 32)) { + throw InvalidWitnessProgramException( + 'Program version $witnessVersion and length of witness program ${witnessProgramUint8List.length} are invalid'); + } + List dataUint5List = BytesUtils.convertBits(witnessProgramUint8List, 8, 5, allowPaddingBool: true); + + List finalUint5List = [witnessVersion] + dataUint5List; + + return Bech32Encoder().encode(Bech32.fromUint5List(segWit.hrp, finalUint5List)); + } +} diff --git a/lib/src/codecs/bech32/segwit_bech32_codec.dart b/lib/src/codecs/bech32/segwit_bech32_codec.dart deleted file mode 100644 index 825eba3..0000000 --- a/lib/src/codecs/bech32/segwit_bech32_codec.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'dart:typed_data'; - -import 'package:bech32/bech32.dart'; - -/// The [SegwitBech32Codec] class is designed for encoding Segregated Witness (SegWit) addresses using the Bech32 encoding scheme, -/// as outlined in BIP-0173 and further refined for SegWit in BIP-0174: -/// https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki -/// https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki -class SegwitBech32Codec { - static String encode(String hrp, int witnessVersion, List witnessProgram) { - // TODO(dominik): Implement custom Segwit encoder - return const SegwitCodec().encode(Segwit(hrp, witnessVersion, witnessProgram)); - } - - static Uint8List decode(String bechAddress) { - // TODO(dominik): Implement custom Segwit decoder - return Uint8List.fromList(const SegwitCodec().decode(bechAddress).program); - } -} diff --git a/lib/src/utils/bytes_utils.dart b/lib/src/utils/bytes_utils.dart new file mode 100644 index 0000000..c72893d --- /dev/null +++ b/lib/src/utils/bytes_utils.dart @@ -0,0 +1,41 @@ +import 'dart:typed_data'; + +class BytesUtils { + /// Converts a list of integers from one bit-grouping to another. + /// + /// This is operation is used to convert between 8-bit bytes and 5-bit groups. + /// + /// - [inputBytesList]: The input list of integers (e.g., 8-bit bytes or 5-bit groups). + /// - [inputBitLength]: The bit length of each element in [dataList] (e.g., 8 for bytes, 5 for Bech32). + /// - [outputBitLength]: The desired bit length of each element in the output list (e.g., 5 for Bech32, 8 for bytes). + /// - [padBool]: If `true`, the result will be padded with zeros to ensure all bits are consumed. + /// If `false`, padding is disallowed, and an error will be thrown if excess + static Uint8List convertBits(List inputBytesList, int inputBitLength, int outputBitLength, {bool allowPaddingBool = true}) { + int acc = 0; + int bits = 0; + List outputBytesList = []; + int outputBitMask = (1 << outputBitLength) - 1; + + for (int inputByte in inputBytesList) { + if (inputByte < 0 || (inputByte >> inputBitLength) != 0) { + throw FormatException('Invalid value $inputBitLength'); + } + acc = (acc << inputBitLength) | inputByte; + bits += inputBitLength; + while (bits >= outputBitLength) { + bits -= outputBitLength; + outputBytesList.add((acc >> bits) & outputBitMask); + } + } + if (allowPaddingBool) { + if (bits > 0) { + outputBytesList.add((acc << (outputBitLength - bits)) & outputBitMask); + } else if (bits >= inputBitLength) { + throw const FormatException('Excess bits require padding, but padding is disallowed.'); + } else if (((acc << (outputBitLength - bits)) & outputBitMask) != 0) { + throw const FormatException('Non-zero padding bits detected.'); + } + } + return Uint8List.fromList(outputBytesList); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index ec1de77..9267116 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.5 +version: 0.0.6 publish_to: none environment: @@ -15,10 +15,6 @@ dependencies: # https://pub.dev/packages/logger logger: 2.4.0 - # Library implementing Bitcoins BIP173 (Bech32 encoding) specification in a Flutter friendly fashion. - # https://pub.dev/packages/bech32 - bech32: 0.2.2 - # Utility functions and classes related to the dart:typed_data library. # https://pub.dev/packages/typed_data typed_data: ^1.3.0 diff --git a/test/unit/codecs/bech32/bech32_codec_test.dart b/test/unit/codecs/bech32/bech32_codec_test.dart deleted file mode 100644 index 663764b..0000000 --- a/test/unit/codecs/bech32/bech32_codec_test.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'dart:convert'; - -import 'package:codec_utils/src/codecs/bech32/bech32_codec.dart'; -import 'package:codec_utils/src/codecs/bech32/bech32_pair.dart'; -import 'package:test/test.dart'; - -void main() { - group('Tests of Bech32Codec.encode()', () { - test('Should [return String] encoded by Bech32', () { - // Arrange - Bech32Pair actualBech32Pair = Bech32Pair(hrp: 'crypto', data: base64Decode('KxmiVli7oFEs8N5rjnzLtw7eym0=')); - - // Act - String actualEncodedData = Bech32Codec.encode(actualBech32Pair); - - // Assert - String expectedEncodedData = 'crypto19vv6y4jchws9zt8sme4culxtku8dajndgyhdm2'; - - expect(actualEncodedData, expectedEncodedData); - }); - }); - - group('Tests of Bech32Codec.decode()', () { - test('Should [return Bech32Pair] decoded by Bech32', () { - // Arrange - String actualDataToDecode = 'crypto19vv6y4jchws9zt8sme4culxtku8dajndgyhdm2'; - - // Act - Bech32Pair actualBech32Pair = Bech32Codec.decode(actualDataToDecode); - - // Assert - Bech32Pair expectedBech32Pair = Bech32Pair(hrp: 'crypto', data: base64Decode('KxmiVli7oFEs8N5rjnzLtw7eym0=')); - - expect(actualBech32Pair, expectedBech32Pair); - }); - }); -} diff --git a/test/unit/codecs/bech32/bech32_decoder_test.dart b/test/unit/codecs/bech32/bech32_decoder_test.dart new file mode 100644 index 0000000..d2dad3f --- /dev/null +++ b/test/unit/codecs/bech32/bech32_decoder_test.dart @@ -0,0 +1,93 @@ +import 'dart:typed_data'; + +import 'package:codec_utils/src/codecs/bech32/bech32.dart'; +import 'package:codec_utils/src/codecs/bech32/bech32_decoder.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_bech32_exception.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_checksum_exception.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_hrp_exception.dart'; +import 'package:test/test.dart'; + +void main() { + group('Tests of Bech32Decoder.decode()', () { + test('Should [return decoded data] for valid Bech32 address', () { + // Arrange + String actualInput = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'; + + // Act + Bech32 actualBech32 = Bech32Decoder().decode(actualInput); + + // Assert + String expectedHrp = 'bc'; + Uint8List expectedDataUint8List = + Uint8List.fromList([3, 168, 243, 183, 64, 204, 140, 182, 162, 164, 160, 226, 46, 141, 157, 25, 31, 138, 25, 222, 176]); + + Bech32 expectedBech32 = Bech32(expectedHrp, expectedDataUint8List); + + expect(actualBech32, expectedBech32); + }); + + test('Should [throw InvalidBech32Exception] if input exceeds maximum length', () { + // Arrange + String actualInput = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + + // Assert + expect(() => Bech32Decoder().decode(actualInput), throwsA(isA())); + }); + + test('Should [throw InvalidBech32Exception] if input contains uppercase characters', () { + // Arrange + String actualInput = 'Bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty'; + + // Assert + expect(() => Bech32Decoder().decode(actualInput), throwsA(isA())); + }); + + test('Should [throw InvalidBech32Exception] if separator "1" is missing', () { + // Arrange + String actualInput = 'bcqw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty'; + + // Assert + expect(() => Bech32Decoder().decode(actualInput), throwsA(isA())); + }); + + test('Should [throw InvalidChecksumException] if checksum is invalid due to invalid trailing characters', () { + // Arrange + String actualInput = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080'; + + // Assert + expect(() => Bech32Decoder().decode(actualInput), throwsA(isA())); + }); + + test('Should [throw InvalidHrpException] if HRP is empty', () { + // Arrange + String actualInput = '1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'; + + // Assert + expect(() => Bech32Decoder().decode(actualInput), throwsA(isA())); + }); + + test('Should [throw InvalidHrpException] if HRP contains invalid characters', () { + // Arrange + String actualInput = '@bc 1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'; + + // Assert + expect(() => Bech32Decoder().decode(actualInput), throwsA(isA())); + }); + + test('Should [throw InvalidChecksumException] if checksum contains invalid character (e.g. "!")', () { + // Arrange + String actualInput = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4t!'; + + // Assert + expect(() => Bech32Decoder().decode(actualInput), throwsA(isA())); + }); + + test('Should [throw InvalidChecksumException] if checksum is incorrect (valid format, invalid sum)', () { + // Arrange + String actualInput = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4tx'; + + // Assert + expect(() => Bech32Decoder().decode(actualInput), throwsA(isA())); + }); + }); +} diff --git a/test/unit/codecs/bech32/bech32_encoder_test.dart b/test/unit/codecs/bech32/bech32_encoder_test.dart new file mode 100644 index 0000000..de57f6d --- /dev/null +++ b/test/unit/codecs/bech32/bech32_encoder_test.dart @@ -0,0 +1,50 @@ +import 'dart:typed_data'; + +import 'package:codec_utils/codec_utils.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_bech32_exception.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_hrp_exception.dart'; +import 'package:test/test.dart'; + +void main() { + group('Tests of Bech32Encoder.encode()', () { + test('Should [return valid Bech32 address] from provided bytes', () { + // Arrange + Uint8List actualUint8List = + Uint8List.fromList([3, 168, 243, 183, 64, 204, 140, 182, 162, 164, 160, 226, 46, 141, 157, 25, 31, 138, 25, 222, 176]); + Bech32 actualBechInput = Bech32('bc', actualUint8List); + + // Act + String actualEncoded = Bech32Encoder().encode(Bech32(actualBechInput.hrp, actualUint8List)); + + // Assert + String expectedEncoded = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'; + + expect(actualEncoded, expectedEncoded); + }); + + test('Should [throw InvalidLengthException] if encoded Bech32 string exceeds 90 characters', () { + // Arrange + Uint8List actualUint8List = Uint8List.fromList([ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + ]); + Bech32 actualInput = Bech32('bc', actualUint8List); + + // Assert + expect(() => Bech32Encoder().encode(actualInput), throwsA(isA())); + }); + + test('Should [throw InvalidHRPException] if HRP is empty', () { + // Arrange + Bech32 actualInput = Bech32('', Uint8List.fromList([0, 1, 2])); + + // Assert + expect( + () => Bech32Encoder().encode(actualInput), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/unit/codecs/bech32/bech32_utils_test.dart b/test/unit/codecs/bech32/bech32_utils_test.dart new file mode 100644 index 0000000..82ff243 --- /dev/null +++ b/test/unit/codecs/bech32/bech32_utils_test.dart @@ -0,0 +1,35 @@ +import 'package:codec_utils/src/codecs/bech32/bech32_utils.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; + +void main() { + group('Tests of Bech32Utils.calculatePolymodChecksum()', () { + test('Should [return polymod checksum] for given data', () { + // Arrange + List actualInputUint5List = [3, 3, 0, 2, 3]; + + // Act + int actualResult = Bech32Utils.calculatePolymodChecksum(actualInputUint5List); + + // Assert + int expectedResult = 36798531; + + expect(actualResult, expectedResult); + }); + }); + + group('Tests of Bech32Utils.expandHrp()', () { + test('Should [return expended HRP] for given data', () { + // Arrange + String actualHrp = 'bc'; + + // Act + List actualUint5List = Bech32Utils.expandHrp(actualHrp); + + // Assert + List expectedUint5List = [3, 3, 0, 2, 3]; + + expect(actualUint5List, expectedUint5List); + }); + }); +} diff --git a/test/unit/codecs/bech32/seg_wit/seg_wit_decoder_test.dart b/test/unit/codecs/bech32/seg_wit/seg_wit_decoder_test.dart new file mode 100644 index 0000000..69c10f2 --- /dev/null +++ b/test/unit/codecs/bech32/seg_wit/seg_wit_decoder_test.dart @@ -0,0 +1,79 @@ +import 'dart:typed_data'; + +import 'package:codec_utils/codec_utils.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_bech32_exception.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_hrp_exception.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_witness_program_exception.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_witness_version_exception.dart'; +import 'package:test/test.dart'; + +void main() { + group('Test of SegWitDecoder.decode()', () { + test('Should [return decoded data] based on given data', () { + // Arrange + String actualData = 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'; + + // Act + SegWit actualDecodedData = SegWitDecoder().decode(actualData); + + // Assert + String expectedHrp = 'bc'; + int expectedWitnessVersion = 0; + Uint8List expectedUint8List = Uint8List.fromList( + [117, 30, 118, 232, 25, 145, 150, 212, 84, 148, 28, 69, 209, 179, 163, 35, 241, 67, 59, 214], + ); + + SegWit expectedDecodedData = SegWit(expectedHrp, expectedWitnessVersion, expectedUint8List); + + expect(actualDecodedData, expectedDecodedData); + }); + + test('Should [throw InvalidBech32Exception] if input string is empty', () { + // Arrange + String actualData = ''; + + // Assert + expect(() => SegWitDecoder().decode(actualData), throwsA(isA())); + }); + + test('Should [throw InvalidHRPException] if HRP contains invalid characters', () { + // Arrange + String actualData = 'zz1pzry9x8gf2tvdw0s3jn5xsa9te'; + + // Assert + expect(() => SegWitDecoder().decode(actualData), throwsA(isA())); + }); + + test('Should [throw FormatException] if witness program is empty after decoding', () { + // Arrange + String actualData = 'bc1gmk9yu'; + + // Assert + expect(() => SegWitDecoder().decode(actualData), throwsA(isA())); + }); + + test('Should [throw InvalidWitnessVersionException] if witness version is out of allowed range (0–16)', () { + // Arrange + String actualData = 'bc13qypqxpq9qcrsszg2pvxq6rs0zqg3yyc5k252qw'; + + // Assert + expect(() => SegWitDecoder().decode(actualData), throwsA(isA())); + }); + + test('Should [throw InvalidProgramException] if witness program length is too short', () { + // Arrange + String actualData = 'bc1qqypf4jh4k'; + + // Assert + expect(() => SegWitDecoder().decode(actualData), throwsA(isA())); + }); + + test('Should [throw InvalidProgramException] if witness program length is too long', () { + // Arrange + String actualData = 'bc1qqypqxpq9qcrsszg2pvxq6rs0zqg3yyc5z5tpwxqergd3c8g7ruszzg3rysjjvfeg9y4zktpd9chnqvfjqaly2h'; + + // Assert + expect(() => SegWitDecoder().decode(actualData), throwsA(isA())); + }); + }); +} diff --git a/test/unit/codecs/bech32/seg_wit/seg_wit_encoder_test.dart b/test/unit/codecs/bech32/seg_wit/seg_wit_encoder_test.dart new file mode 100644 index 0000000..bd571be --- /dev/null +++ b/test/unit/codecs/bech32/seg_wit/seg_wit_encoder_test.dart @@ -0,0 +1,71 @@ +import 'dart:typed_data'; + +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_witness_program_exception.dart'; +import 'package:codec_utils/src/codecs/bech32/exceptions/invalid_witness_version_exception.dart'; +import 'package:codec_utils/src/codecs/bech32/seg_wit/seg_wit.dart'; +import 'package:codec_utils/src/codecs/bech32/seg_wit/seg_wit_encoder.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; + +void main() { + group('Tests of SegWitEncoder.encode()', () { + test('Should [return converted data] based on given data', () { + // Arrange + Uint8List actualUint8List = Uint8List.fromList([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]); + SegWit actualSegWit = SegWit('bc', 0, actualUint8List); + + // Act + String actualEncodedData = SegWitEncoder().encode(actualSegWit); + + // Assert + String expectedEncodedData = 'bc1qqqqsyqcyq5rqwzqfpg9scrgwpugpzysn4v0345'; + + expect(actualEncodedData, expectedEncodedData); + }); + + test('Should [throw InvalidWitnessVersionException] if witness version is greater than 16', () { + // Arrange + Uint8List actualUint8List = Uint8List.fromList([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]); + SegWit actualSegWit = SegWit('bc', 17, actualUint8List); + + // Assert + expect(() => SegWitEncoder().encode(actualSegWit), throwsA(isA())); + }); + + test('Should [throw InvalidProgramException] if witness program length is less than 2', () { + // Arrange + Uint8List actualUint8List = Uint8List.fromList([0]); + SegWit actualSegWit = SegWit('bc', 0, actualUint8List); + + // Assert + expect(() => SegWitEncoder().encode(actualSegWit), throwsA(isA())); + }); + + test('Should [throw InvalidProgramException] if witness program length exceeds 40 bytes', () { + // Arrange + Uint8List actualUint8List = Uint8List.fromList([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, // + 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 + ]); + + SegWit actualSegWit = SegWit('bc', 0, actualUint8List); + + // Assert + expect(() => SegWitEncoder().encode(actualSegWit), throwsA(isA())); + }); + + test('Should [throw InvalidProgramException] if witness program length is 31 bytes (invalid for version 0)', () { + // Arrange + Uint8List actualUint8List = Uint8List.fromList([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, // + 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29 + ]); + + SegWit actualSegWit = SegWit('bc', 0, actualUint8List); + + // Assert + expect(() => SegWitEncoder().encode(actualSegWit), throwsA(isA())); + }); + }); +} diff --git a/test/unit/codecs/bech32/segwit_bech32_codec_test.dart b/test/unit/codecs/bech32/segwit_bech32_codec_test.dart deleted file mode 100644 index d898b2f..0000000 --- a/test/unit/codecs/bech32/segwit_bech32_codec_test.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:codec_utils/src/codecs/bech32/segwit_bech32_codec.dart'; -import 'package:test/test.dart'; - -void main() { - group('Tests of SegwitBech32Codec.encode()', () { - test('Should [return String] encoded by Bech32 (Segwit version)', () { - // Arrange - Uint8List actualDataToEncode = base64Decode('KxmiVli7oFEs8N5rjnzLtw7eym0='); - - // Act - String actualEncodedData = SegwitBech32Codec.encode('bc', 0, actualDataToEncode); - - // Assert - String expectedEncodedData = 'bc1q9vv6y4jchws9zt8sme4culxtku8dajnd5jq660'; - - expect(actualEncodedData, expectedEncodedData); - }); - }); - - group('Tests of SegwitBech32Codec.decode()', () { - test('Should [return String] encoded by Bech32 (Segwit version)', () { - // Arrange - String actualDataToDecode = 'bc1q9vv6y4jchws9zt8sme4culxtku8dajnd5jq660'; - - // Act - Uint8List actualDecodedData = SegwitBech32Codec.decode(actualDataToDecode); - - // Assert - Uint8List expectedDecodedData = base64Decode('KxmiVli7oFEs8N5rjnzLtw7eym0='); - - expect(actualDecodedData, expectedDecodedData); - }); - }); -} diff --git a/test/unit/utils/bytes_utils_test.dart b/test/unit/utils/bytes_utils_test.dart new file mode 100644 index 0000000..0e906f0 --- /dev/null +++ b/test/unit/utils/bytes_utils_test.dart @@ -0,0 +1,62 @@ +import 'package:codec_utils/src/utils/bytes_utils.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; + +void main() { + group('Tests of BytesUtils.convertBits()', () { + test('Should [return converted bits] from 8 to 5 with padding', () { + // Arrange + List actualInputList = [255]; + + // Act + List actualOutputList = BytesUtils.convertBits(actualInputList, 8, 5, allowPaddingBool: true); + + // Assert + List expectedOutputList = [31, 28]; + + expect(actualOutputList, expectedOutputList); + }); + + test('Should [return converted bits] from 8 to 5 without padding', () { + // Arrange + List actualInputList = [30]; + + // Act + List actualOutputList = BytesUtils.convertBits(actualInputList, 8, 5, allowPaddingBool: false); + + // Assert + List expectedOutputList = [3]; + + expect(actualOutputList, expectedOutputList); + }); + + test('Should [return converted bits] from 5 to 8 with padding', () { + // Arrange + List actualInputList = [3, 30]; + + // Act + List actualOutputList = BytesUtils.convertBits(actualInputList, 5, 8, allowPaddingBool: true); + + // Assert + List expectedOutputList = [31, 128]; + + expect(actualOutputList, expectedOutputList); + }); + + test('Should [throw FormatException] for invalid input', () { + // Arrange + List actualInputList = [-1]; + + // Assert + expect(() => BytesUtils.convertBits(actualInputList, 8, 5, allowPaddingBool: true), throwsA(isA())); + }); + + test('Should [throw FormatException] for invalid input', () { + // Arrange + List actualInputList = [256]; + + // Assert + expect(() => BytesUtils.convertBits(actualInputList, 8, 5, allowPaddingBool: true), throwsA(isA())); + }); + }); +}