From cbce3e54d88d64dd41ca339d2df176b586561f34 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sat, 28 Sep 2024 01:03:15 +0800 Subject: [PATCH 01/12] =?UTF-8?q?=F0=9F=9A=80=20Implement=20subaccount=20f?= =?UTF-8?q?or=20the=20Principal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/principal/principal.dart | 133 ++++++++++++++++++++++++++--------- lib/wallet/rosetta.dart | 5 +- test/agent/cbor.dart | 2 +- 3 files changed, 104 insertions(+), 36 deletions(-) diff --git a/lib/principal/principal.dart b/lib/principal/principal.dart index bdb9bf6d..c104cad6 100644 --- a/lib/principal/principal.dart +++ b/lib/principal/principal.dart @@ -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'; @@ -16,7 +15,11 @@ const _maxLengthInBytes = 29; const _typeOpaque = 1; class Principal { - const Principal(this._arr); + const Principal( + this._arr, { + Uint8List? subAccount, + }) : assert(subAccount == null || subAccount.length == 32), + _subAccount = subAccount; factory Principal.selfAuthenticating(Uint8List publicKey) { final sha = sha224Hash(publicKey.buffer); @@ -28,18 +31,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 && 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._arr, 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, @@ -49,40 +52,83 @@ 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); - } - - factory Principal.fromUint8Array(Uint8List arr) { - return Principal(arr); - } - final Uint8List _arr; + final Uint8List? _subAccount; bool isAnonymous() { return _arr.lengthInBytes == 1 && _arr[0] == _suffixAnonymous; @@ -90,14 +136,10 @@ class Principal { Uint8List toUint8List() => _arr; - Uint8List toBlob() => toUint8List(); - String toHex() => _toHexString(_arr).toUpperCase(); String toText() { - final checksumArrayBuf = ByteData(4); - checksumArrayBuf.setUint32(0, getCrc32(_arr.buffer)); - final checksum = checksumArrayBuf.buffer.asUint8List(); + final checksum = _getChecksum(_arr.buffer); final bytes = Uint8List.fromList(_arr); final array = Uint8List.fromList([...checksum, ...bytes]); final result = base32Encode(array); @@ -107,7 +149,23 @@ 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 != null) { + final checksum = base32Encode( + _getChecksum(Uint8List.fromList(_arr + _subAccount!).buffer), + ); + buffer.write('-$checksum'); + int i = 0; + while (_subAccount![i] == 0 && i < _subAccount!.length) { + i++; + } + final subAccount = _subAccount!.sublist(i, _subAccount!.length); + if (subAccount.isNotEmpty) { + buffer.write('.'); + buffer.write(subAccount.toHex().replaceFirst('0', '')); + } + } + return buffer.toString(); } Uint8List toAccountId({Uint8List? subAccount}) { @@ -120,7 +178,7 @@ class Principal { } final hash = SHA224(); hash.update('\x0Aaccount-id'.plainToU8a()); - hash.update(toBlob()); + hash.update(toUint8List()); hash.update(subAccount ?? Uint8List(32)); final data = hash.digest(); final view = ByteData(4); @@ -137,14 +195,18 @@ class Principal { @override bool operator ==(Object other) => - identical(this, other) || other is Principal && _arr.eq(other._arr); + identical(this, other) || + other is Principal && + _arr.eq(other._arr) && + (_subAccount?.eq(other._subAccount ?? Uint8List(0)) ?? + _subAccount == null && other._subAccount == null); @override - int get hashCode => _arr.hashCode; + int get hashCode => Object.hash(_arr, _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 @@ -164,11 +226,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(); } diff --git a/lib/wallet/rosetta.dart b/lib/wallet/rosetta.dart index 51986cdd..6f0be261 100644 --- a/lib/wallet/rosetta.dart +++ b/lib/wallet/rosetta.dart @@ -609,11 +609,10 @@ Map 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(), diff --git a/test/agent/cbor.dart b/test/agent/cbor.dart index 7b01b2b0..68d50027 100644 --- a/test/agent/cbor.dart +++ b/test/agent/cbor.dart @@ -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'); }); } From 81d6e9b81d773e71d7b076d94bb6bb4ad86a131c Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sat, 28 Sep 2024 01:03:29 +0800 Subject: [PATCH 02/12] =?UTF-8?q?=E2=9C=85=20Add=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/principal/principal.dart | 52 +++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/test/principal/principal.dart b/test/principal/principal.dart index 66062a9a..4ea64c5c 100644 --- a/test/principal/principal.dart +++ b/test/principal/principal.dart @@ -25,6 +25,58 @@ void principalTest() { expect(Principal.fromText('aaaaa-aa').toHex(), ''); expect(Principal.fromText('2vxsx-fae').toHex(), '04'); expect(Principal.fromText('2vxsx-fae').isAnonymous(), true); + + // ICRC-1 + expect( + Principal.fromText( + 'k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae', + ).toText(), + 'k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae', + ); + expect( + () => Principal.fromText( + 'k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-q6bn32y.', + ).toText(), + throwsA( + isError( + 'The representation is not canonical: ' + 'default subaccount should be omitted.', + ), + ), + ); + expect( + Principal.fromText( + 'k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-6cc627i.1', + ).toText(), + 'k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-6cc627i.1', + ); + expect( + () => Principal.fromText( + 'k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-6cc627i.01', + ).toText(), + throwsA( + isError( + 'The representation is not canonical: ' + 'leading zeros are not allowed in subaccounts.', + ), + ), + ); + expect( + () => Principal.fromText( + 'k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae.1', + ).toText(), + throwsA( + isError('Missing checksum'), + ), + ); + expect( + Principal.fromText( + 'k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-dfxgiyy' + '.102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20', + ).toText(), + 'k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-dfxgiyy' + '.102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20', + ); }); test('errors out on invalid checksums', () { From 98103e6a357e9b9db4806c983a56993c355da761 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sat, 28 Sep 2024 01:05:24 +0800 Subject: [PATCH 03/12] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Expose=20`Principal.?= =?UTF-8?q?subAccount`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/principal/principal.dart | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/principal/principal.dart b/lib/principal/principal.dart index c104cad6..9804d538 100644 --- a/lib/principal/principal.dart +++ b/lib/principal/principal.dart @@ -17,9 +17,8 @@ const _typeOpaque = 1; class Principal { const Principal( this._arr, { - Uint8List? subAccount, - }) : assert(subAccount == null || subAccount.length == 32), - _subAccount = subAccount; + this.subAccount, + }) : assert(subAccount == null || subAccount.length == 32); factory Principal.selfAuthenticating(Uint8List publicKey) { final sha = sha224Hash(publicKey.buffer); @@ -37,7 +36,7 @@ class Principal { } else if (other is Map && other['_isPrincipal'] == true) { return Principal(other['_arr'], subAccount: other['_subAccount']); } else if (other is Principal) { - return Principal(other._arr, subAccount: other._subAccount); + return Principal(other._arr, subAccount: other.subAccount); } throw UnreachableError(); } @@ -128,7 +127,7 @@ class Principal { } final Uint8List _arr; - final Uint8List? _subAccount; + final Uint8List? subAccount; bool isAnonymous() { return _arr.lengthInBytes == 1 && _arr[0] == _suffixAnonymous; @@ -150,19 +149,19 @@ class Principal { throw StateError('No characters found.'); } final buffer = StringBuffer(matches.map((e) => e.group(0)).join('-')); - if (_subAccount != null) { + if (subAccount != null) { final checksum = base32Encode( - _getChecksum(Uint8List.fromList(_arr + _subAccount!).buffer), + _getChecksum(Uint8List.fromList(_arr + subAccount!).buffer), ); buffer.write('-$checksum'); int i = 0; - while (_subAccount![i] == 0 && i < _subAccount!.length) { + while (subAccount![i] == 0 && i < subAccount!.length) { i++; } - final subAccount = _subAccount!.sublist(i, _subAccount!.length); - if (subAccount.isNotEmpty) { + final subAccountBuffer = subAccount!.sublist(i, subAccount!.length); + if (subAccountBuffer.isNotEmpty) { buffer.write('.'); - buffer.write(subAccount.toHex().replaceFirst('0', '')); + buffer.write(subAccountBuffer.toHex().replaceFirst('0', '')); } } return buffer.toString(); @@ -198,11 +197,11 @@ class Principal { identical(this, other) || other is Principal && _arr.eq(other._arr) && - (_subAccount?.eq(other._subAccount ?? Uint8List(0)) ?? - _subAccount == null && other._subAccount == null); + (subAccount?.eq(other.subAccount ?? Uint8List(0)) ?? + subAccount == null && other.subAccount == null); @override - int get hashCode => Object.hash(_arr, _subAccount); + int get hashCode => Object.hash(_arr, subAccount); } class CanisterId extends Principal { From 7dcb5b4d2458ceb53de874ac67a1f37a1fb91334 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sun, 29 Sep 2024 00:44:17 +0800 Subject: [PATCH 04/12] =?UTF-8?q?=F0=9F=9A=80=20Use=20the=20inner=20subacc?= =?UTF-8?q?ount=20when=20converting=20pid=20to=20aid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/agent/auth.dart | 6 ++---- lib/principal/principal.dart | 9 +-------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/lib/agent/auth.dart b/lib/agent/auth.dart index e10388bd..072b85f6 100644 --- a/lib/agent/auth.dart +++ b/lib/agent/auth.dart @@ -49,10 +49,8 @@ abstract class SignIdentity implements Identity { /// Signs a blob of data, with this identity's private key. Future 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 diff --git a/lib/principal/principal.dart b/lib/principal/principal.dart index 9804d538..490ec483 100644 --- a/lib/principal/principal.dart +++ b/lib/principal/principal.dart @@ -167,14 +167,7 @@ class Principal { 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(toUint8List()); From 212a935e6729f1fa80e0d060fb95b7f040264e07 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sun, 29 Sep 2024 01:12:42 +0800 Subject: [PATCH 05/12] =?UTF-8?q?=F0=9F=9A=80=20Improve=20contructors=20an?= =?UTF-8?q?d=20copy=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/principal/principal.dart | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/principal/principal.dart b/lib/principal/principal.dart index 490ec483..4a225b0d 100644 --- a/lib/principal/principal.dart +++ b/lib/principal/principal.dart @@ -16,7 +16,7 @@ const _typeOpaque = 1; class Principal { const Principal( - this._arr, { + this.principal, { this.subAccount, }) : assert(subAccount == null || subAccount.length == 32); @@ -36,7 +36,7 @@ class Principal { } else if (other is Map && other['_isPrincipal'] == true) { return Principal(other['_arr'], subAccount: other['_subAccount']); } else if (other is Principal) { - return Principal(other._arr, subAccount: other.subAccount); + return Principal(other.principal, subAccount: other.subAccount); } throw UnreachableError(); } @@ -126,20 +126,27 @@ class Principal { return principal; } - final Uint8List _arr; + final Uint8List principal; final Uint8List? subAccount; + Principal newSubAccount(Uint8List subAccount) { + 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 toUint8List() => principal; - String toHex() => _toHexString(_arr).toUpperCase(); + String toHex() => _toHexString(principal).toUpperCase(); String toText() { - final checksum = _getChecksum(_arr.buffer); - 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}'); @@ -151,7 +158,7 @@ class Principal { final buffer = StringBuffer(matches.map((e) => e.group(0)).join('-')); if (subAccount != null) { final checksum = base32Encode( - _getChecksum(Uint8List.fromList(_arr + subAccount!).buffer), + _getChecksum(Uint8List.fromList(principal + subAccount!).buffer), ); buffer.write('-$checksum'); int i = 0; @@ -189,12 +196,12 @@ class Principal { bool operator ==(Object other) => identical(this, other) || other is Principal && - _arr.eq(other._arr) && + principal.eq(other.principal) && (subAccount?.eq(other.subAccount ?? Uint8List(0)) ?? subAccount == null && other.subAccount == null); @override - int get hashCode => Object.hash(_arr, subAccount); + int get hashCode => Object.hash(principal, subAccount); } class CanisterId extends Principal { From 450d352a04248d6c5da67383ebfbccabcc97de5a Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sun, 29 Sep 2024 01:18:06 +0800 Subject: [PATCH 06/12] =?UTF-8?q?=F0=9F=90=9B=20Fix=20archive=20encoder=20?= =?UTF-8?q?signature=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/archiver/encoder.dart | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/archiver/encoder.dart b/lib/archiver/encoder.dart index b46db518..70417a48 100644 --- a/lib/archiver/encoder.dart +++ b/lib/archiver/encoder.dart @@ -159,9 +159,9 @@ class SingingBlockZipFileEncoder extends ZipFileEncoder { } @override - void close() { + Future close() { _encoder.writeBlock(_output); _encoder.endEncode(); - _output.close(); + return _output.close(); } } diff --git a/pubspec.yaml b/pubspec.yaml index f6c56179..801488ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ environment: flutter: '>=3.10.0' dependencies: - archive: ^3.3.8 + archive: ^3.6.0 args: ^2.3.1 bip32: ^2.0.0 bip39: ^1.0.6 From 35e61f80238489082155e0a28fa2323819a771a8 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sun, 29 Sep 2024 01:31:38 +0800 Subject: [PATCH 07/12] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Principal=20toText?= =?UTF-8?q?=20bytes=20strip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/principal/principal.dart | 15 +++++++++------ test/principal/principal.dart | 8 ++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/principal/principal.dart b/lib/principal/principal.dart index 4a225b0d..1cfb4279 100644 --- a/lib/principal/principal.dart +++ b/lib/principal/principal.dart @@ -161,14 +161,17 @@ class Principal { _getChecksum(Uint8List.fromList(principal + subAccount!).buffer), ); buffer.write('-$checksum'); - int i = 0; - while (subAccount![i] == 0 && i < subAccount!.length) { - i++; + final subAccountHex = subAccount!.toHex(); + int nonZeroStart = 0; + while (nonZeroStart < subAccountHex.length) { + if (subAccountHex[nonZeroStart] != '0') { + break; + } + nonZeroStart++; } - final subAccountBuffer = subAccount!.sublist(i, subAccount!.length); - if (subAccountBuffer.isNotEmpty) { + if (nonZeroStart != subAccountHex.length) { buffer.write('.'); - buffer.write(subAccountBuffer.toHex().replaceFirst('0', '')); + buffer.write(subAccountHex.replaceRange(0, nonZeroStart, '')); } } return buffer.toString(); diff --git a/test/principal/principal.dart b/test/principal/principal.dart index 4ea64c5c..65fe76b5 100644 --- a/test/principal/principal.dart +++ b/test/principal/principal.dart @@ -77,6 +77,14 @@ void principalTest() { 'k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-dfxgiyy' '.102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20', ); + expect( + Principal.fromText( + 'z4s7u-byaaa-aaaao-a3paa-cai-l435n4y' + '.1de15338bedf41b306d4ef5615d1f94fd7b0474b9255690cbe78d2309f02', + ).toText(), + 'z4s7u-byaaa-aaaao-a3paa-cai-l435n4y' + '.1de15338bedf41b306d4ef5615d1f94fd7b0474b9255690cbe78d2309f02', + ); }); test('errors out on invalid checksums', () { From bc73e515705aeac91b3f4910f66745f49d271443 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sun, 29 Sep 2024 09:58:43 +0800 Subject: [PATCH 08/12] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Allows=20empty=20sub?= =?UTF-8?q?account=20income?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/principal/principal.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/principal/principal.dart b/lib/principal/principal.dart index 1cfb4279..333bbdd1 100644 --- a/lib/principal/principal.dart +++ b/lib/principal/principal.dart @@ -129,7 +129,10 @@ class Principal { final Uint8List principal; final Uint8List? subAccount; - Principal newSubAccount(Uint8List subAccount) { + Principal newSubAccount(Uint8List? subAccount) { + if (subAccount == null) { + return this; + } if (this.subAccount == null || !this.subAccount!.eq(subAccount)) { return Principal(principal, subAccount: subAccount); } From 6017e504f7d4b3952477d48f0499aae0d5d44753 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sun, 29 Sep 2024 09:58:55 +0800 Subject: [PATCH 09/12] =?UTF-8?q?=E2=9C=85=20Fix=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pubspec.yaml | 2 +- test/principal/principal.dart | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index c266a26b..558edc95 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,7 +10,7 @@ environment: sdk: '>=3.0.0 <4.0.0' flutter: '>=3.10.0' -dependencies +dependencies: archive: ^3.5.0 args: ^2.3.1 bip32: ^2.0.0 diff --git a/test/principal/principal.dart b/test/principal/principal.dart index 65fe76b5..97e6f62d 100644 --- a/test/principal/principal.dart +++ b/test/principal/principal.dart @@ -93,20 +93,24 @@ void principalTest() { () => Principal.fromText('0chl6-4hpzw-vqaaa-aaaaa-c').toHex(), throwsA( isError( - 'Principal expected to be 2chl6-4hpzw-vqaaa-aaaaa-c but got', + 'The principal is expected to be 2chl6-4hpzw-vqaaa-aaaaa-c but got', ), ), ); expect( () => Principal.fromText('0aaaa-aa').toHex(), throwsA( - isError('Principal expected to be aaaaa-aa but got'), + isError( + 'The principal is expected to be aaaaa-aa but got', + ), ), ); expect( () => Principal.fromText('0vxsx-fae').toHex(), throwsA( - isError('Principal expected to be 2vxsx-fae but got'), + isError( + 'The principal is expected to be 2vxsx-fae but got', + ), ), ); }); From 103ea31f50d337cc8619e54471eab5670a137d2d Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sun, 29 Sep 2024 10:30:02 +0800 Subject: [PATCH 10/12] =?UTF-8?q?=F0=9F=90=9B=20Ignores=20empty=20subaccou?= =?UTF-8?q?nt=20and=20fixes=20checksum=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/principal/principal.dart | 21 +++++++++++++-------- test/principal/principal.dart | 9 +++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/principal/principal.dart b/lib/principal/principal.dart index 333bbdd1..a94971e7 100644 --- a/lib/principal/principal.dart +++ b/lib/principal/principal.dart @@ -14,11 +14,16 @@ const _suffixAnonymous = 4; const _maxLengthInBytes = 29; const _typeOpaque = 1; +final _emptySubAccount = Uint8List(32); + class Principal { - const Principal( + Principal( this.principal, { - this.subAccount, - }) : assert(subAccount == null || subAccount.length == 32); + Uint8List? subAccount, + }) : assert(subAccount == null || subAccount.length == 32), + subAccount = subAccount != null && subAccount.eq(_emptySubAccount) + ? null + : subAccount; factory Principal.selfAuthenticating(Uint8List publicKey) { final sha = sha224Hash(publicKey.buffer); @@ -130,7 +135,7 @@ class Principal { final Uint8List? subAccount; Principal newSubAccount(Uint8List? subAccount) { - if (subAccount == null) { + if (subAccount == null || subAccount.eq(_emptySubAccount)) { return this; } if (this.subAccount == null || !this.subAccount!.eq(subAccount)) { @@ -160,10 +165,6 @@ class Principal { } final buffer = StringBuffer(matches.map((e) => e.group(0)).join('-')); if (subAccount != null) { - final checksum = base32Encode( - _getChecksum(Uint8List.fromList(principal + subAccount!).buffer), - ); - buffer.write('-$checksum'); final subAccountHex = subAccount!.toHex(); int nonZeroStart = 0; while (nonZeroStart < subAccountHex.length) { @@ -173,6 +174,10 @@ class Principal { 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, '')); } diff --git a/test/principal/principal.dart b/test/principal/principal.dart index 97e6f62d..e45a06d2 100644 --- a/test/principal/principal.dart +++ b/test/principal/principal.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:agent_dart/principal/principal.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -85,6 +87,13 @@ void principalTest() { 'z4s7u-byaaa-aaaao-a3paa-cai-l435n4y' '.1de15338bedf41b306d4ef5615d1f94fd7b0474b9255690cbe78d2309f02', ); + expect( + Principal( + Uint8List.fromList([0, 0, 0, 0, 0, 224, 17, 26, 1, 1]), + subAccount: Uint8List(32), + ).toText(), + 'lrllq-iqaaa-aaaah-acena-cai', + ); }); test('errors out on invalid checksums', () { From 66aec4280ddd859e7b354b75f6e92772f6f96a5a Mon Sep 17 00:00:00 2001 From: Alex Li Date: Sun, 29 Sep 2024 11:40:57 +0800 Subject: [PATCH 11/12] =?UTF-8?q?=F0=9F=94=96=201.0.0-dev.24?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 +++++ pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8e692ca..d38d1421 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/pubspec.yaml b/pubspec.yaml index 558edc95..0d5a2974 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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' From 10d300440ca146d9f10746bba82e355a1b1eabe2 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Mon, 30 Sep 2024 11:36:49 +0800 Subject: [PATCH 12/12] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Improve=20empty=20su?= =?UTF-8?q?baccount?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/principal/principal.dart | 50 ++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/lib/principal/principal.dart b/lib/principal/principal.dart index a94971e7..d935fc45 100644 --- a/lib/principal/principal.dart +++ b/lib/principal/principal.dart @@ -17,13 +17,11 @@ const _typeOpaque = 1; final _emptySubAccount = Uint8List(32); class Principal { - Principal( - this.principal, { + const Principal( + this._principal, { Uint8List? subAccount, }) : assert(subAccount == null || subAccount.length == 32), - subAccount = subAccount != null && subAccount.eq(_emptySubAccount) - ? null - : subAccount; + _subAccount = subAccount; factory Principal.selfAuthenticating(Uint8List publicKey) { final sha = sha224Hash(publicKey.buffer); @@ -41,7 +39,7 @@ class Principal { } else if (other is Map && other['_isPrincipal'] == true) { return Principal(other['_arr'], subAccount: other['_subAccount']); } else if (other is Principal) { - return Principal(other.principal, subAccount: other.subAccount); + return Principal(other._principal, subAccount: other.subAccount); } throw UnreachableError(); } @@ -131,30 +129,37 @@ class Principal { return principal; } - final Uint8List principal; - final Uint8List? subAccount; + final Uint8List _principal; + final Uint8List? _subAccount; + + Uint8List? get subAccount { + if (_subAccount case final v when v == null || v.eq(_emptySubAccount)) { + return null; + } + return _subAccount; + } 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 Principal(_principal, subAccount: subAccount); } return this; } bool isAnonymous() { - return principal.lengthInBytes == 1 && principal[0] == _suffixAnonymous; + return _principal.lengthInBytes == 1 && _principal[0] == _suffixAnonymous; } - Uint8List toUint8List() => principal; + Uint8List toUint8List() => _principal; - String toHex() => _toHexString(principal).toUpperCase(); + String toHex() => _toHexString(_principal).toUpperCase(); String toText() { - final checksum = _getChecksum(principal.buffer); - final bytes = Uint8List.fromList(principal); + 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}'); @@ -164,8 +169,9 @@ class Principal { throw StateError('No characters found.'); } final buffer = StringBuffer(matches.map((e) => e.group(0)).join('-')); - if (subAccount != null) { - final subAccountHex = subAccount!.toHex(); + if (_subAccount case final subAccount? + when !subAccount.eq(_emptySubAccount)) { + final subAccountHex = subAccount.toHex(); int nonZeroStart = 0; while (nonZeroStart < subAccountHex.length) { if (subAccountHex[nonZeroStart] != '0') { @@ -175,7 +181,7 @@ class Principal { } if (nonZeroStart != subAccountHex.length) { final checksum = base32Encode( - _getChecksum(Uint8List.fromList(principal + subAccount!).buffer), + _getChecksum(Uint8List.fromList(_principal + subAccount).buffer), ); buffer.write('-$checksum'); buffer.write('.'); @@ -189,7 +195,7 @@ class Principal { final hash = SHA224(); hash.update('\x0Aaccount-id'.plainToU8a()); hash.update(toUint8List()); - hash.update(subAccount ?? Uint8List(32)); + hash.update(subAccount ?? _emptySubAccount); final data = hash.digest(); final view = ByteData(4); view.setUint32(0, getCrc32(data.buffer)); @@ -207,12 +213,12 @@ class Principal { bool operator ==(Object other) => identical(this, other) || other is Principal && - principal.eq(other.principal) && - (subAccount?.eq(other.subAccount ?? Uint8List(0)) ?? - subAccount == null && other.subAccount == null); + _principal.eq(other._principal) && + (_subAccount?.eq(other._subAccount ?? _emptySubAccount) ?? + _subAccount == null && other._subAccount == null); @override - int get hashCode => Object.hash(principal, subAccount); + int get hashCode => Object.hash(_principal, subAccount); } class CanisterId extends Principal {