Skip to content

Commit

Permalink
🚀 [ICRC-1] Implement Principal.subAccount (#81)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexV525 authored Oct 5, 2024
2 parents f50971d + 10d3004 commit 0e0cc1d
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 57 deletions.
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

0 comments on commit 0e0cc1d

Please sign in to comment.