From d343b76a358b2b2b930c531481fc52f46871e67f Mon Sep 17 00:00:00 2001 From: Bin <17426470+boyan01@users.noreply.github.com> Date: Mon, 15 Jan 2024 14:50:50 +0800 Subject: [PATCH] improve transaction to multi uuid receiver (#125) * add multisigs api * improve send to multi receiver * update readme --- CHANGELOG.md | 5 +++ example/common.dart | 2 +- example/pubspec.lock | 2 +- example/safe_transfer.dart | 3 +- lib/src/api/utxo_api.dart | 67 +++++++++++++++++++++++++++++++++++-- lib/src/util/address.dart | 48 ++++++++++++++++---------- pubspec.yaml | 2 +- test/util/address_test.dart | 8 ++--- 8 files changed, 108 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7884c68..20554d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.1.0 + +* improve transaction to multi uuid receiver +* add transaction to MixAddress + ## 1.0.4 * make `getOutputs()` members parameter nullable diff --git a/example/common.dart b/example/common.dart index c061313..d63a7e6 100644 --- a/example/common.dart +++ b/example/common.dart @@ -17,7 +17,7 @@ late Client client = Client( privateKey: privateKey, sessionId: botSessionId, userId: botUserId, -); +)..dio.options.extra['retry'] = true; const tronUSDT = 'b91e18ff-a9ae-3dc7-8679-e935d9a4b34b'; const cnb = '965e5c6e-434c-3fa9-b780-c50f43cd955c'; diff --git a/example/pubspec.lock b/example/pubspec.lock index bbed0c9..693ad09 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -159,7 +159,7 @@ packages: path: ".." relative: true source: path - version: "1.0.4" + version: "1.1.0" path: dependency: transitive description: diff --git a/example/safe_transfer.dart b/example/safe_transfer.dart index 2002cb9..d994bc0 100644 --- a/example/safe_transfer.dart +++ b/example/safe_transfer.dart @@ -9,8 +9,9 @@ Future main() async { ); print('cnb balance: ${balance}'); + // transaction to one user await client.utxoApi.transactionToUser( - userId: 'cfb018b0-eaf7-40ec-9e07-28a5158f1269', + receiverIds: ['cfb018b0-eaf7-40ec-9e07-28a5158f1269'], amount: '0.0001', asset: Token.cnb.assetId, spendKey: hex.encode(spendKey.bytes), diff --git a/lib/src/api/utxo_api.dart b/lib/src/api/utxo_api.dart index e8bc6b2..bbad69d 100644 --- a/lib/src/api/utxo_api.dart +++ b/lib/src/api/utxo_api.dart @@ -186,6 +186,35 @@ class UtxoApi { TransactionResponse.fromJson, ); + Future> getMultisigs(String requestId) => + MixinResponse.request( + dio.get( + '/safe/multisigs/$requestId', + ), + TransactionResponse.fromJson, + ); + + Future> signTransactionMultisigs( + String requestId, + TransactionRequest request, + ) => + MixinResponse.request( + dio.post( + '/safe/multisigs/$requestId/sign', + data: request.toJson(), + ), + TransactionResponse.fromJson, + ); + + Future> unlockTransactionMultisigs( + String requestId) => + MixinResponse.request( + dio.post( + '/safe/multisigs/$requestId/unlock', + ), + TransactionResponse.fromJson, + ); + Future assetBalance({ required String assetId, required int threshold, @@ -219,6 +248,7 @@ class UtxoApi { required String asset, required int threshold, required Decimal desiredAmount, + List members = const [], }) async { int? latestSequence; final outputs = []; @@ -229,6 +259,7 @@ class UtxoApi { threshold: threshold, asset: asset, state: OutputState.unspent.name, + members: members, limit: limit, offset: latestSequence == null ? null : latestSequence + 1, )) @@ -264,29 +295,38 @@ class UtxoApi { /// Send a tx to mixin user. /// - /// [userId] destination user uuid + /// [receiverIds] destination user uuid list + /// [senderIds] sender user uuid list + /// /// [spendKey] spend key hex + /// Future> transactionToUser({ - required String userId, + required List receiverIds, required String amount, required String asset, required String spendKey, + List senderIds = const [], int threshold = 1, String? memo, }) async { + assert(receiverIds.isNotEmpty, 'receiverIds is empty'); + receiverIds = receiverIds.toList()..sort(); + final (utxos, change) = await _getEnoughOutputsForTransaction( asset: asset, threshold: threshold, desiredAmount: Decimal.parse(amount), + members: senderIds, ); final recipients = [ UserTransactionRecipient( - members: [userId], + members: receiverIds, threshold: threshold, amount: amount, ), if (change > Decimal.zero) + // change UserTransactionRecipient( members: utxos[0].receivers, threshold: utxos[0].receiversThreshold, @@ -607,4 +647,25 @@ class UtxoApi { return sentTx.data; } } + + Future> transactionToMixAddress({ + required MixAddress address, + required String amount, + required String spendKey, + required String asset, + }) async { + assert(address.members.isNotEmpty, 'members is empty'); + switch (address.memberType) { + case MixMemberType.uuid: + return transactionToUser( + receiverIds: address.members, + amount: amount, + asset: asset, + spendKey: spendKey, + ); + case MixMemberType.xin: + assert(address.members.length == 1, 'members length is not 1'); + throw UnimplementedError(); + } + } } diff --git a/lib/src/util/address.dart b/lib/src/util/address.dart index 5568aef..52f8f68 100644 --- a/lib/src/util/address.dart +++ b/lib/src/util/address.dart @@ -11,7 +11,7 @@ const kMixAddressVersion = 2; const kMainAddressPrefix = 'XIN'; const kMixAddressPrefix = 'MIX'; -enum _MemberType { +enum MixMemberType { xin, uuid, } @@ -55,11 +55,29 @@ String getMainnetAddressFromPublic(Uint8List pub) { } class MixAddress { - MixAddress({ + MixAddress._private({ required this.members, required this.threshold, + required this.memberType, }); + factory MixAddress.fromUuid(List members, int threshold) => + MixAddress._private( + members: members, + threshold: threshold, + memberType: MixMemberType.uuid, + ); + + factory MixAddress.fromMainNetMixAddress( + List members, + int threshold, + ) => + MixAddress._private( + members: members, + threshold: threshold, + memberType: MixMemberType.xin, + ); + static MixAddress? tryParse(String address) { try { return parse(address); @@ -105,27 +123,20 @@ class MixAddress { final memberData = Uint8List.sublistView(payload, 3); final members = []; + if (memberData.length == total * 16) { for (var i = 0; i < total; i++) { final id = Uuid.unparse(memberData, offset: i * 16); members.add(id); } - return MixAddress( - members: members, - threshold: threshold, - ); - } - - if (memberData.length == total * 64) { + return MixAddress.fromUuid(members, threshold); + } else if (memberData.length == total * 64) { for (var i = 0; i < total; i++) { final pub = Uint8List.sublistView(memberData, i * 64, (i + 1) * 64); final address = getMainnetAddressFromPublic(pub); members.add(address); } - return MixAddress( - members: members, - threshold: threshold, - ); + return MixAddress.fromMainNetMixAddress(members, threshold); } throw Exception('invalid member data. $memberData'); @@ -133,6 +144,7 @@ class MixAddress { final List members; final int threshold; + final MixMemberType memberType; String toAddress() { if (members.length > 255) { @@ -149,12 +161,12 @@ class MixAddress { ]); final memberData = []; - _MemberType? type; + MixMemberType? type; for (final member in members) { if (member.startsWith(kMainAddressPrefix)) { if (type == null) { - type = _MemberType.xin; - } else if (type != _MemberType.xin) { + type = MixMemberType.xin; + } else if (type != MixMemberType.xin) { throw Exception('invalid member type. $member'); } final pub = getPublicFromMainnetAddress(member); @@ -164,8 +176,8 @@ class MixAddress { memberData.add(pub); } else { if (type == null) { - type = _MemberType.uuid; - } else if (type != _MemberType.uuid) { + type = MixMemberType.uuid; + } else if (type != MixMemberType.uuid) { throw Exception('invalid member type. $member'); } try { diff --git a/pubspec.yaml b/pubspec.yaml index dadfcff..ab3d485 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: mixin_bot_sdk_dart description: Mixin Messenger API for Dart/Flutter, build decentrialized applications on Mixin -version: 1.0.4 +version: 1.1.0 homepage: https://github.com/MixinNetwork/mixin_bot_sdk_dart environment: diff --git a/test/util/address_test.dart b/test/util/address_test.dart index 05fbd81..7217fdf 100644 --- a/test/util/address_test.dart +++ b/test/util/address_test.dart @@ -4,7 +4,7 @@ import 'package:test/test.dart'; void main() { test('test 1', () { final members = ['67a87828-18f5-46a1-b6cc-c72a97a77c43']; - final address = MixAddress(members: members, threshold: 1).toAddress(); + final address = MixAddress.fromUuid(members, 1).toAddress(); expect('MIX3QEeg1WkLrjvjxyMQf6Xc8dxs81tpPc', address); final ma = MixAddress.parse('MIX3QEeg1WkLrjvjxyMQf6Xc8dxs81tpPc'); @@ -23,7 +23,7 @@ void main() { 'c6d0c728-2624-429b-8e0d-d9d19b6592fa', '67a87828-18f5-46a1-b6cc-c72a97a77c43', ]; - final address = MixAddress(members: members, threshold: 4).toAddress(); + final address = MixAddress.fromUuid(members, 4).toAddress(); expect( 'MIX4fwusRK88p5GexHWddUQuYJbKMJTAuBvhudgahRXKndvaM8FdPHS2Hgeo7DQxNVoSkKSEDyZeD8TYBhiwiea9PvCzay1A9Vx1C2nugc4iAmhwLGGv4h3GnABeCXHTwWEto9wEe1MWB49jLzy3nuoM81tqE2XnLvUWv', address, @@ -40,7 +40,7 @@ void main() { final members = [ 'XIN3BMNy9pQyj5XWDJtTbaBVE2zQ66zBo2weyc43iL286asdqwApWswAzQC5qba26fh3fzHK9iMoxyx1q3Lgj45KJftzGD9q' ]; - final address = MixAddress(members: members, threshold: 1).toAddress(); + final address = MixAddress.fromMainNetMixAddress(members, 1).toAddress(); expect( 'MIXPYWwhjxKsbFRzAP2Dcb2mMjj7sQQo4MpCSv3NYaYCdQ2kEcbcimpPT81gaxtuNhunLWPx7Sv7fawjZ8DhRmEj8E2hrQM4Z6e', address); @@ -57,7 +57,7 @@ void main() { 'XINMd9kCbxEoEetZuDM8gGJS11X3TVrRLwzhnqgMr65qjJBkCncNqSAngESpC7Hddnsw1D9Jo2QJakbFPr8WyrM6VkskGkB8', 'XINLM7VuMYSjvKiEQPyLpaG7NDLDPngWWFBZpVJjhGamMsgPbmeSsGs3fQzNoqSr6syBTyLM3i69T7iSN8Tru7aQadiKLkSV', ]; - final address = MixAddress(members: members, threshold: 2).toAddress(); + final address = MixAddress.fromMainNetMixAddress(members, 2).toAddress(); expect( 'MIXBCirWksVv9nuphqbtNRZZvwKsXHHMUnB5hVrVY1P7f4eBdLpDoLwiQoHYPvXia2wFepnX6hJwTjHybzBiroWVEMaFHeRFfLpcU244tzRM8smak9iRAD4PJRHN1MLHRWFtErottp9t7piaRVZBzsQXpSsaSgagj93voQdUuXhuQGZNj3Fme5YYMHfJBWjoRFHis4mnhBgxkyEGRUHAVYnfej2FhrypJmMDu74irRTdj2xjQYr6ovBJSUBYDBcvAyLPE3cEKc4JsPz7b9', address,