Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🚀 [ICRC-1] Implement Principal.subAccount #81

Merged
merged 13 commits into from
Oct 5, 2024
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ that can be found in the LICENSE file. -->

# Changelog

## 1.0.0-dev.24

- Implement subaccount as `Principal.subAccount`, which also removes the `subAccount` parameter
when converting a principal to an Account ID. Some other constructors are also removed due to duplicates.

## 1.0.0-dev.23

- Fix encoder with deps and format files.
Expand Down
6 changes: 2 additions & 4 deletions lib/agent/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,8 @@ abstract class SignIdentity implements Identity {
/// Signs a blob of data, with this identity's private key.
Future<BinaryBlob> sign(BinaryBlob blob);

Uint8List getAccountId([Uint8List? subAccount]) {
return Principal.selfAuthenticating(
getPublicKey().toDer(),
).toAccountId(subAccount: subAccount);
Uint8List getAccountId() {
return Principal.selfAuthenticating(getPublicKey().toDer()).toAccountId();
}

/// Get the principal represented by this identity. Normally should be a
Expand Down
4 changes: 2 additions & 2 deletions lib/archiver/encoder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,9 @@ class SingingBlockZipFileEncoder extends ZipFileEncoder {
}

@override
Future<void> close() async {
Future<void> close() {
_encoder.writeBlock(_output);
_encoder.endEncode();
await _output.close();
return _output.close();
}
}
171 changes: 128 additions & 43 deletions lib/principal/principal.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'dart:typed_data';

import 'package:agent_dart/agent/types.dart';
import 'package:agent_dart/utils/extension.dart';

import '../agent/errors.dart';
Expand All @@ -15,8 +14,14 @@ const _suffixAnonymous = 4;
const _maxLengthInBytes = 29;
const _typeOpaque = 1;

final _emptySubAccount = Uint8List(32);

class Principal {
const Principal(this._arr);
const Principal(
this._principal, {
Uint8List? subAccount,
}) : assert(subAccount == null || subAccount.length == 32),
_subAccount = subAccount;

factory Principal.selfAuthenticating(Uint8List publicKey) {
final sha = sha224Hash(publicKey.buffer);
Expand All @@ -28,18 +33,18 @@ class Principal {
return Principal(Uint8List.fromList([_suffixAnonymous]));
}

factory Principal.from(dynamic other) {
factory Principal.from(Object? other) {
if (other is String) {
return Principal.fromText(other);
} else if (other is Map<String, dynamic> && other['_isPrincipal'] == true) {
return Principal(other['_arr']);
return Principal(other['_arr'], subAccount: other['_subAccount']);
} else if (other is Principal) {
return Principal(other._arr);
return Principal(other._principal, subAccount: other.subAccount);
}
throw UnreachableError();
}

factory Principal.create(int uSize, Uint8List data) {
factory Principal.create(int uSize, Uint8List data, Uint8List? subAccount) {
if (uSize > data.length) {
throw RangeError.range(
uSize,
Expand All @@ -49,56 +54,112 @@ class Principal {
'Size must within the data length',
);
}
return Principal.fromBlob(data.sublist(0, uSize));
return Principal(data.sublist(0, uSize), subAccount: subAccount);
}

factory Principal.fromHex(String hex) {
factory Principal.fromHex(String hex, {String? subAccountHex}) {
if (hex.isEmpty) {
return Principal(Uint8List(0));
}
return Principal(hex.toU8a());
if (subAccountHex == null || subAccountHex.isEmpty) {
subAccountHex = null;
} else if (subAccountHex.startsWith('0')) {
throw ArgumentError.value(
subAccountHex,
'subAccountHex',
'The representation is not canonical: '
'leading zeros are not allowed in subaccounts.',
);
}
return Principal(
hex.toU8a(),
subAccount: subAccountHex?.padLeft(64, '0').toU8a(),
);
}

factory Principal.fromText(String text) {
final canisterIdNoDash = text.toLowerCase().replaceAll('-', '');
if (text.endsWith('.')) {
throw ArgumentError(
'The representation is not canonical: '
'default subaccount should be omitted.',
);
}
final paths = text.split('.');
final String? subAccountHex;
if (paths.length > 1) {
subAccountHex = paths.last;
} else {
subAccountHex = null;
}
if (subAccountHex != null && subAccountHex.startsWith('0')) {
throw ArgumentError.value(
subAccountHex,
'subAccount',
'The representation is not canonical: '
'leading zeros are not allowed in subaccounts.',
);
}
String prePrincipal = paths.first;
// Removes the checksum if sub-account is valid.
if (subAccountHex != null) {
final list = prePrincipal.split('-');
final checksum = list.removeLast();
// Checksum is 7 digits.
if (checksum.length != 7) {
throw ArgumentError.value(
prePrincipal,
'principal',
'Missing checksum',
);
}
prePrincipal = list.join('-');
}
final canisterIdNoDash = prePrincipal.toLowerCase().replaceAll('-', '');
Uint8List arr = base32Decode(canisterIdNoDash);
arr = arr.sublist(4, arr.length);
final principal = Principal(arr);
final subAccount = subAccountHex?.padLeft(64, '0').toU8a();
final principal = Principal(arr, subAccount: subAccount);
if (principal.toText() != text) {
throw ArgumentError.value(
text,
'Principal',
'Principal expected to be ${principal.toText()} but got',
'principal',
'The principal is expected to be ${principal.toText()} but got',
);
}
return principal;
}

factory Principal.fromBlob(BinaryBlob arr) {
return Principal.fromUint8Array(arr);
}
final Uint8List _principal;
final Uint8List? _subAccount;

factory Principal.fromUint8Array(Uint8List arr) {
return Principal(arr);
Uint8List? get subAccount {
if (_subAccount case final v when v == null || v.eq(_emptySubAccount)) {
return null;
}
return _subAccount;
}

final Uint8List _arr;
Principal newSubAccount(Uint8List? subAccount) {
if (subAccount == null || subAccount.eq(_emptySubAccount)) {
return this;
}
if (this.subAccount == null || !this.subAccount!.eq(subAccount)) {
return Principal(_principal, subAccount: subAccount);
}
return this;
}

bool isAnonymous() {
return _arr.lengthInBytes == 1 && _arr[0] == _suffixAnonymous;
return _principal.lengthInBytes == 1 && _principal[0] == _suffixAnonymous;
}

Uint8List toUint8List() => _arr;

Uint8List toBlob() => toUint8List();
Uint8List toUint8List() => _principal;

String toHex() => _toHexString(_arr).toUpperCase();
String toHex() => _toHexString(_principal).toUpperCase();

String toText() {
final checksumArrayBuf = ByteData(4);
checksumArrayBuf.setUint32(0, getCrc32(_arr.buffer));
final checksum = checksumArrayBuf.buffer.asUint8List();
final bytes = Uint8List.fromList(_arr);
final checksum = _getChecksum(_principal.buffer);
final bytes = Uint8List.fromList(_principal);
final array = Uint8List.fromList([...checksum, ...bytes]);
final result = base32Encode(array);
final reg = RegExp(r'.{1,5}');
Expand All @@ -107,21 +168,34 @@ class Principal {
// This should only happen if there's no character, which is unreachable.
throw StateError('No characters found.');
}
return matches.map((e) => e.group(0)).join('-');
final buffer = StringBuffer(matches.map((e) => e.group(0)).join('-'));
if (_subAccount case final subAccount?
when !subAccount.eq(_emptySubAccount)) {
final subAccountHex = subAccount.toHex();
int nonZeroStart = 0;
while (nonZeroStart < subAccountHex.length) {
if (subAccountHex[nonZeroStart] != '0') {
break;
}
nonZeroStart++;
}
if (nonZeroStart != subAccountHex.length) {
final checksum = base32Encode(
_getChecksum(Uint8List.fromList(_principal + subAccount).buffer),
);
buffer.write('-$checksum');
buffer.write('.');
buffer.write(subAccountHex.replaceRange(0, nonZeroStart, ''));
}
}
return buffer.toString();
}

Uint8List toAccountId({Uint8List? subAccount}) {
if (subAccount != null && subAccount.length != 32) {
throw ArgumentError.value(
subAccount,
'subAccount',
'Sub-account address must be 32-bytes length',
);
}
Uint8List toAccountId() {
final hash = SHA224();
hash.update('\x0Aaccount-id'.plainToU8a());
hash.update(toBlob());
hash.update(subAccount ?? Uint8List(32));
hash.update(toUint8List());
hash.update(subAccount ?? _emptySubAccount);
final data = hash.digest();
final view = ByteData(4);
view.setUint32(0, getCrc32(data.buffer));
Expand All @@ -137,14 +211,18 @@ class Principal {

@override
bool operator ==(Object other) =>
identical(this, other) || other is Principal && _arr.eq(other._arr);
identical(this, other) ||
other is Principal &&
_principal.eq(other._principal) &&
(_subAccount?.eq(other._subAccount ?? _emptySubAccount) ??
_subAccount == null && other._subAccount == null);

@override
int get hashCode => _arr.hashCode;
int get hashCode => Object.hash(_principal, subAccount);
}

class CanisterId extends Principal {
CanisterId(Principal pid) : super(pid.toBlob());
CanisterId(Principal pid) : super(pid.toUint8List());

factory CanisterId.fromU64(int val) {
// It is important to use big endian here to ensure that the generated
Expand All @@ -164,11 +242,18 @@ class CanisterId extends Principal {

data[blobLength] = _typeOpaque;
return CanisterId(
Principal.create(blobLength + 1, Uint8List.fromList(data)),
Principal.create(blobLength + 1, Uint8List.fromList(data), null),
);
}
}

Uint8List _getChecksum(ByteBuffer buffer) {
final checksumArrayBuf = ByteData(4);
checksumArrayBuf.setUint32(0, getCrc32(buffer));
final checksum = checksumArrayBuf.buffer.asUint8List();
return checksum;
}

String _toHexString(Uint8List bytes) {
return bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join();
}
5 changes: 2 additions & 3 deletions lib/wallet/rosetta.dart
Original file line number Diff line number Diff line change
Expand Up @@ -609,11 +609,10 @@ Map<String, dynamic> transactionDecoder(String txnHash) {
final content = envelope['content'] as Map;
final senderPubkey = envelope['sender_pubkey'];
final sendArgs = SendRequest.fromBuffer(content['arg']);
final senderAddress =
Principal.fromBlob(Uint8List.fromList(content['sender']));
final senderAddress = Principal(Uint8List.fromList(content['sender']));
final hash = SHA224()
..update(('\x0Aaccount-id').plainToU8a())
..update(senderAddress.toBlob())
..update(senderAddress.toUint8List())
..update(Uint8List(32));
return {
'from': hash.digest(),
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: |
a plugin package for dart and flutter apps.
Developers can build ones to interact with Dfinity's blockchain directly.
repository: https://github.com/AstroxNetwork/agent_dart
version: 1.0.0-dev.23
version: 1.0.0-dev.24

environment:
sdk: '>=3.0.0 <4.0.0'
Expand Down
2 changes: 1 addition & 1 deletion test/agent/cbor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@ void cborTest() {
final outputA = output['a'] as Uint8Buffer;

expect(outputA.toHex(), inputA.toHex());
expect(Principal.fromUint8Array(outputA.toU8a()).toText(), 'aaaaa-aa');
expect(Principal(outputA.toU8a()).toText(), 'aaaaa-aa');
});
}
Loading
Loading