Skip to content

Commit 2bbd2c7

Browse files
authored
2 parents cf09266 + 84e3e81 commit 2bbd2c7

File tree

8 files changed

+217
-57
lines changed

8 files changed

+217
-57
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ that can be found in the LICENSE file. -->
44

55
# Changelog
66

7+
## 1.0.0-dev.24
8+
9+
- Implement subaccount as `Principal.subAccount`, which also removes the `subAccount` parameter
10+
when converting a principal to an Account ID. Some other constructors are also removed due to duplicates.
11+
712
## 1.0.0-dev.23
813

914
- Fix encoder with deps and format files.

lib/agent/auth.dart

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,8 @@ abstract class SignIdentity implements Identity {
4949
/// Signs a blob of data, with this identity's private key.
5050
Future<BinaryBlob> sign(BinaryBlob blob);
5151

52-
Uint8List getAccountId([Uint8List? subAccount]) {
53-
return Principal.selfAuthenticating(
54-
getPublicKey().toDer(),
55-
).toAccountId(subAccount: subAccount);
52+
Uint8List getAccountId() {
53+
return Principal.selfAuthenticating(getPublicKey().toDer()).toAccountId();
5654
}
5755

5856
/// Get the principal represented by this identity. Normally should be a

lib/archiver/encoder.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,9 @@ class SingingBlockZipFileEncoder extends ZipFileEncoder {
159159
}
160160

161161
@override
162-
Future<void> close() async {
162+
Future<void> close() {
163163
_encoder.writeBlock(_output);
164164
_encoder.endEncode();
165-
await _output.close();
165+
return _output.close();
166166
}
167167
}

lib/principal/principal.dart

Lines changed: 128 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import 'dart:typed_data';
22

3-
import 'package:agent_dart/agent/types.dart';
43
import 'package:agent_dart/utils/extension.dart';
54

65
import '../agent/errors.dart';
@@ -15,8 +14,14 @@ const _suffixAnonymous = 4;
1514
const _maxLengthInBytes = 29;
1615
const _typeOpaque = 1;
1716

17+
final _emptySubAccount = Uint8List(32);
18+
1819
class Principal {
19-
const Principal(this._arr);
20+
const Principal(
21+
this._principal, {
22+
Uint8List? subAccount,
23+
}) : assert(subAccount == null || subAccount.length == 32),
24+
_subAccount = subAccount;
2025

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

31-
factory Principal.from(dynamic other) {
36+
factory Principal.from(Object? other) {
3237
if (other is String) {
3338
return Principal.fromText(other);
3439
} else if (other is Map<String, dynamic> && other['_isPrincipal'] == true) {
35-
return Principal(other['_arr']);
40+
return Principal(other['_arr'], subAccount: other['_subAccount']);
3641
} else if (other is Principal) {
37-
return Principal(other._arr);
42+
return Principal(other._principal, subAccount: other.subAccount);
3843
}
3944
throw UnreachableError();
4045
}
4146

42-
factory Principal.create(int uSize, Uint8List data) {
47+
factory Principal.create(int uSize, Uint8List data, Uint8List? subAccount) {
4348
if (uSize > data.length) {
4449
throw RangeError.range(
4550
uSize,
@@ -49,56 +54,112 @@ class Principal {
4954
'Size must within the data length',
5055
);
5156
}
52-
return Principal.fromBlob(data.sublist(0, uSize));
57+
return Principal(data.sublist(0, uSize), subAccount: subAccount);
5358
}
5459

55-
factory Principal.fromHex(String hex) {
60+
factory Principal.fromHex(String hex, {String? subAccountHex}) {
5661
if (hex.isEmpty) {
5762
return Principal(Uint8List(0));
5863
}
59-
return Principal(hex.toU8a());
64+
if (subAccountHex == null || subAccountHex.isEmpty) {
65+
subAccountHex = null;
66+
} else if (subAccountHex.startsWith('0')) {
67+
throw ArgumentError.value(
68+
subAccountHex,
69+
'subAccountHex',
70+
'The representation is not canonical: '
71+
'leading zeros are not allowed in subaccounts.',
72+
);
73+
}
74+
return Principal(
75+
hex.toU8a(),
76+
subAccount: subAccountHex?.padLeft(64, '0').toU8a(),
77+
);
6078
}
6179

6280
factory Principal.fromText(String text) {
63-
final canisterIdNoDash = text.toLowerCase().replaceAll('-', '');
81+
if (text.endsWith('.')) {
82+
throw ArgumentError(
83+
'The representation is not canonical: '
84+
'default subaccount should be omitted.',
85+
);
86+
}
87+
final paths = text.split('.');
88+
final String? subAccountHex;
89+
if (paths.length > 1) {
90+
subAccountHex = paths.last;
91+
} else {
92+
subAccountHex = null;
93+
}
94+
if (subAccountHex != null && subAccountHex.startsWith('0')) {
95+
throw ArgumentError.value(
96+
subAccountHex,
97+
'subAccount',
98+
'The representation is not canonical: '
99+
'leading zeros are not allowed in subaccounts.',
100+
);
101+
}
102+
String prePrincipal = paths.first;
103+
// Removes the checksum if sub-account is valid.
104+
if (subAccountHex != null) {
105+
final list = prePrincipal.split('-');
106+
final checksum = list.removeLast();
107+
// Checksum is 7 digits.
108+
if (checksum.length != 7) {
109+
throw ArgumentError.value(
110+
prePrincipal,
111+
'principal',
112+
'Missing checksum',
113+
);
114+
}
115+
prePrincipal = list.join('-');
116+
}
117+
final canisterIdNoDash = prePrincipal.toLowerCase().replaceAll('-', '');
64118
Uint8List arr = base32Decode(canisterIdNoDash);
65119
arr = arr.sublist(4, arr.length);
66-
final principal = Principal(arr);
120+
final subAccount = subAccountHex?.padLeft(64, '0').toU8a();
121+
final principal = Principal(arr, subAccount: subAccount);
67122
if (principal.toText() != text) {
68123
throw ArgumentError.value(
69124
text,
70-
'Principal',
71-
'Principal expected to be ${principal.toText()} but got',
125+
'principal',
126+
'The principal is expected to be ${principal.toText()} but got',
72127
);
73128
}
74129
return principal;
75130
}
76131

77-
factory Principal.fromBlob(BinaryBlob arr) {
78-
return Principal.fromUint8Array(arr);
79-
}
132+
final Uint8List _principal;
133+
final Uint8List? _subAccount;
80134

81-
factory Principal.fromUint8Array(Uint8List arr) {
82-
return Principal(arr);
135+
Uint8List? get subAccount {
136+
if (_subAccount case final v when v == null || v.eq(_emptySubAccount)) {
137+
return null;
138+
}
139+
return _subAccount;
83140
}
84141

85-
final Uint8List _arr;
142+
Principal newSubAccount(Uint8List? subAccount) {
143+
if (subAccount == null || subAccount.eq(_emptySubAccount)) {
144+
return this;
145+
}
146+
if (this.subAccount == null || !this.subAccount!.eq(subAccount)) {
147+
return Principal(_principal, subAccount: subAccount);
148+
}
149+
return this;
150+
}
86151

87152
bool isAnonymous() {
88-
return _arr.lengthInBytes == 1 && _arr[0] == _suffixAnonymous;
153+
return _principal.lengthInBytes == 1 && _principal[0] == _suffixAnonymous;
89154
}
90155

91-
Uint8List toUint8List() => _arr;
92-
93-
Uint8List toBlob() => toUint8List();
156+
Uint8List toUint8List() => _principal;
94157

95-
String toHex() => _toHexString(_arr).toUpperCase();
158+
String toHex() => _toHexString(_principal).toUpperCase();
96159

97160
String toText() {
98-
final checksumArrayBuf = ByteData(4);
99-
checksumArrayBuf.setUint32(0, getCrc32(_arr.buffer));
100-
final checksum = checksumArrayBuf.buffer.asUint8List();
101-
final bytes = Uint8List.fromList(_arr);
161+
final checksum = _getChecksum(_principal.buffer);
162+
final bytes = Uint8List.fromList(_principal);
102163
final array = Uint8List.fromList([...checksum, ...bytes]);
103164
final result = base32Encode(array);
104165
final reg = RegExp(r'.{1,5}');
@@ -107,21 +168,34 @@ class Principal {
107168
// This should only happen if there's no character, which is unreachable.
108169
throw StateError('No characters found.');
109170
}
110-
return matches.map((e) => e.group(0)).join('-');
171+
final buffer = StringBuffer(matches.map((e) => e.group(0)).join('-'));
172+
if (_subAccount case final subAccount?
173+
when !subAccount.eq(_emptySubAccount)) {
174+
final subAccountHex = subAccount.toHex();
175+
int nonZeroStart = 0;
176+
while (nonZeroStart < subAccountHex.length) {
177+
if (subAccountHex[nonZeroStart] != '0') {
178+
break;
179+
}
180+
nonZeroStart++;
181+
}
182+
if (nonZeroStart != subAccountHex.length) {
183+
final checksum = base32Encode(
184+
_getChecksum(Uint8List.fromList(_principal + subAccount).buffer),
185+
);
186+
buffer.write('-$checksum');
187+
buffer.write('.');
188+
buffer.write(subAccountHex.replaceRange(0, nonZeroStart, ''));
189+
}
190+
}
191+
return buffer.toString();
111192
}
112193

113-
Uint8List toAccountId({Uint8List? subAccount}) {
114-
if (subAccount != null && subAccount.length != 32) {
115-
throw ArgumentError.value(
116-
subAccount,
117-
'subAccount',
118-
'Sub-account address must be 32-bytes length',
119-
);
120-
}
194+
Uint8List toAccountId() {
121195
final hash = SHA224();
122196
hash.update('\x0Aaccount-id'.plainToU8a());
123-
hash.update(toBlob());
124-
hash.update(subAccount ?? Uint8List(32));
197+
hash.update(toUint8List());
198+
hash.update(subAccount ?? _emptySubAccount);
125199
final data = hash.digest();
126200
final view = ByteData(4);
127201
view.setUint32(0, getCrc32(data.buffer));
@@ -137,14 +211,18 @@ class Principal {
137211

138212
@override
139213
bool operator ==(Object other) =>
140-
identical(this, other) || other is Principal && _arr.eq(other._arr);
214+
identical(this, other) ||
215+
other is Principal &&
216+
_principal.eq(other._principal) &&
217+
(_subAccount?.eq(other._subAccount ?? _emptySubAccount) ??
218+
_subAccount == null && other._subAccount == null);
141219

142220
@override
143-
int get hashCode => _arr.hashCode;
221+
int get hashCode => Object.hash(_principal, subAccount);
144222
}
145223

146224
class CanisterId extends Principal {
147-
CanisterId(Principal pid) : super(pid.toBlob());
225+
CanisterId(Principal pid) : super(pid.toUint8List());
148226

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

165243
data[blobLength] = _typeOpaque;
166244
return CanisterId(
167-
Principal.create(blobLength + 1, Uint8List.fromList(data)),
245+
Principal.create(blobLength + 1, Uint8List.fromList(data), null),
168246
);
169247
}
170248
}
171249

250+
Uint8List _getChecksum(ByteBuffer buffer) {
251+
final checksumArrayBuf = ByteData(4);
252+
checksumArrayBuf.setUint32(0, getCrc32(buffer));
253+
final checksum = checksumArrayBuf.buffer.asUint8List();
254+
return checksum;
255+
}
256+
172257
String _toHexString(Uint8List bytes) {
173258
return bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join();
174259
}

lib/wallet/rosetta.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -609,11 +609,10 @@ Map<String, dynamic> transactionDecoder(String txnHash) {
609609
final content = envelope['content'] as Map;
610610
final senderPubkey = envelope['sender_pubkey'];
611611
final sendArgs = SendRequest.fromBuffer(content['arg']);
612-
final senderAddress =
613-
Principal.fromBlob(Uint8List.fromList(content['sender']));
612+
final senderAddress = Principal(Uint8List.fromList(content['sender']));
614613
final hash = SHA224()
615614
..update(('\x0Aaccount-id').plainToU8a())
616-
..update(senderAddress.toBlob())
615+
..update(senderAddress.toUint8List())
617616
..update(Uint8List(32));
618617
return {
619618
'from': hash.digest(),

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: |
44
a plugin package for dart and flutter apps.
55
Developers can build ones to interact with Dfinity's blockchain directly.
66
repository: https://github.com/AstroxNetwork/agent_dart
7-
version: 1.0.0-dev.23
7+
version: 1.0.0-dev.24
88

99
environment:
1010
sdk: '>=3.0.0 <4.0.0'

test/agent/cbor.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,6 @@ void cborTest() {
4949
final outputA = output['a'] as Uint8Buffer;
5050

5151
expect(outputA.toHex(), inputA.toHex());
52-
expect(Principal.fromUint8Array(outputA.toU8a()).toText(), 'aaaaa-aa');
52+
expect(Principal(outputA.toU8a()).toText(), 'aaaaa-aa');
5353
});
5454
}

0 commit comments

Comments
 (0)