Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 68 additions & 49 deletions lib/codec_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export 'src/codecs/base/base58_codec.dart';

/// Classes designed for encoding data using the Bech32 encoding scheme.
/// Usage:
/// ``
/// ```
/// List<int> convertedUint5List = BytesUtils.convertBits(Bech32.uint8List, 8, 5, padBool: true);
///
/// Bech32 bech32 = Bech32.fromUint5List('bc', convertedUint5List)
Expand All @@ -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(<int>[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(<int>[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:
/// ```
Expand All @@ -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<String> parts = urEncoder.encodeWhole();
/// // Encode whole data to transfer into UR format
/// List<String> 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';
5 changes: 4 additions & 1 deletion lib/src/codecs/base/base58_codec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,10 @@ class Base58Codec {
}
}

return Uint8List.fromList(<int>[...List<int>.filled(padLen, 0), ...bytes]);
return Uint8List.fromList(<int>[
...List<int>.filled(padLen, 0),
if ((bytes[0] == 0 && bytes.length == 1) == false) ...bytes,
]);
}

static List<int> _computeChecksum(Uint8List dataBytes) {
Expand Down
32 changes: 32 additions & 0 deletions lib/src/codecs/byte_reader/byte_reader.dart
Original file line number Diff line number Diff line change
@@ -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;
}
}
6 changes: 6 additions & 0 deletions lib/src/codecs/cbor/a_cbor_tagged_object.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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}');
}
Expand Down
7 changes: 6 additions & 1 deletion lib/src/codecs/cbor/cbor_special_tag.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
85 changes: 85 additions & 0 deletions lib/src/codecs/cbor/crypto/cbor_crypto_multi_accounts.dart
Original file line number Diff line number Diff line change
@@ -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<CborCryptoHDKey> 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<CborMap>().map(CborCryptoHDKey.fromCborMap).toList() ?? <CborCryptoHDKey>[],
device: cborDevice?.toString(),
deviceId: cborDeviceId?.toString(),
deviceVersion: cborVersion?.toString(),
);
}

@override
CborMap toCborMap({required bool includeTagBool}) {
return CborMap.of(
<CborValue, CborValue>{
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 ? <int>[cborSpecialTag.tag] : <int>[],
);
}

@override
CborSpecialTag getCborSpecialTag() => cborSpecialTag;

@override
List<Object?> get props => <Object?>[masterFingerprint, cryptoHDKeyList, device, deviceId, deviceVersion];
}
4 changes: 4 additions & 0 deletions lib/src/codecs/cbor/export.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Loading