Skip to content

Commit

Permalink
MultiSig Implementation (#417)
Browse files Browse the repository at this point in the history
* Rebase

* requested changes

* multisig preparation

* multi sig functionality

* refactor

* Update tests.yml

Remove duplicate entry

* signing test with multisig implementation

* completed multisig implementation

* remove multisig from polkadart_keyring

* formatting

* fix errors

* important changes

* formatting and few changes

* bug fix

* docs improvement

* change docs

* improve example

* change keypair in example

* improve docs and example

---------

Co-authored-by: Leonardo Custodio <[email protected]>
  • Loading branch information
justkawal and leonardocustodio authored Feb 20, 2024
1 parent bed5df8 commit 017cb28
Show file tree
Hide file tree
Showing 41 changed files with 1,525 additions and 177 deletions.
16 changes: 9 additions & 7 deletions examples/bin/extrinsic_demo.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import 'dart:typed_data';

import 'package:convert/convert.dart';
import 'package:polkadart/polkadart.dart'
show
AuthorApi,
Extrinsic,
ExtrinsicPayload,
Provider,
SignatureType,
SigningPayload,
Expand Down Expand Up @@ -39,11 +41,11 @@ Future<void> main(List<String> arguments) async {
final keyring = await KeyPair.sr25519.fromMnemonic(
"resource mirror lecture smooth midnight muffin position cup pepper fruit vanish also//0"); // This is a random key

final publicKey = hex.encode(keyring.publicKey.bytes);
final publicKey = keyring.publicKey.bytes;
print('Public Key: $publicKey');
final dest = $MultiAddress().id(hex.decode(publicKey));
final dest = $MultiAddress().id(publicKey);
final runtimeCall = api.tx.balances.transferAll(dest: dest, keepAlive: true);
final encodedCall = hex.encode(runtimeCall.encode());
final encodedCall = runtimeCall.encode();
print('Encoded call: $encodedCall');

final payloadToSign = SigningPayload(
Expand All @@ -65,10 +67,10 @@ Future<void> main(List<String> arguments) async {
final hexSignature = hex.encode(signature);
print('Signature: $hexSignature');

final extrinsic = Extrinsic(
signer: publicKey,
final extrinsic = ExtrinsicPayload(
signer: Uint8List.fromList(publicKey),
method: encodedCall,
signature: hexSignature,
signature: signature,
eraPeriod: 64,
blockNumber: blockNumber,
nonce: 0,
Expand Down
96 changes: 96 additions & 0 deletions packages/polkadart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,102 @@ void main() async {
}
```

## Multisig

### Initiate Multisig (Create and Fund)

```dart
final provider = Provider.fromUri(Uri.parse('wss://westend-rpc.polkadot.io'));
// Signatory 1
final keypairS1 = await keyring.KeyPair.sr25519.fromUri('//keypairS1');
// Signatory 2
final signatory2Address = '5Cp4.......';
// Signatory 3
final signatory3Address = '5Dt2.......';
// Recipient's Address
final recipientAddress = '5Fq3.......';
///
/// Create and Fund Multisig
final multiSigResponse = await Multisig.createAndFundMultisig(
depositorKeyPair: keypairS1,
otherSignatoriesAddressList: [signatory2Address, signatory3Address],
threshold: 2,
recipientAddress: recipientAddress,
amount: BigInt.parse('7000000000000'), // 7 WND ~ (tokenDecimals: 12)
provider: provider,
);
// Json Response for forwarding to other signatories.
final json = multiSigResponse.toJson();
```

### ApproveAsMulti
#### Used to approve the multisig call and then it waits for other singatories to approve.

```dart
// Signatory 2
final keypairS2 = await keyring.KeyPair.sr25519.fromUri('//keypairS2');
// Json for forwarding to other signatories.
final json = multiSigResponse.toJson();
// Assuming keypairS2 is the first signatory who is approving.
final localResponse = MultisigResponse.fromJson(json);
// Waiting for approx 15 seconds for the transaction to be included in the block.
await Future.delayed(Duration(seconds: 15));
// Approve this call by keypairS2 and wait on further approval.
await localResponse.approveAsMulti(provider, keypairS2);
```

### AsMulti
#### Used to do the final approval of the multisig call and execute the transaction if the threshold is met.

```dart
// Signatory 3
// Assuming keypairS3 is the first signatory who is approving.
final keypairS3 = await keyring.KeyPair.sr25519.fromUri('//keypairS3');
// Json for forwarding to other signatories.
final json = multiSigResponse.toJson();
final localResponse = MultisigResponse.fromJson(json);
// Waiting for approx 15 seconds for the transaction to be included in the block.
await Future.delayed(Duration(seconds: 15));
// Approve this call by keypairS3 and execute the transaction if the threshold is met
await localResponse.asMulti(provider, keypairS3);
```

### cancelAsMulti
#### Used to do cancel the multisig call.

```dart
// Signatory 1
// Assuming keypairS1 is the first signatory who is approving.
final keypairS1 = await keyring.KeyPair.sr25519.fromUri('//keypairS1');
// Json for forwarding to other signatories.
final json = multiSigResponse.toJson();
final localResponse = MultisigResponse.fromJson(json);
// Waiting for approx 15 seconds for the transaction to be included in the block.
await Future.delayed(Duration(seconds: 15));
// Approve this call by keypairS1 and execute the transaction if the threshold is met
await localResponse.asMulti(provider, keypairS1);
```

## Tutorials

Looking for tutorials to get started? Look at [example](./example) for guides on how to use the API to make queries and submit transactions.
70 changes: 70 additions & 0 deletions packages/polkadart/example/multisig_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'package:polkadart/multisig/multisig_base.dart';
import 'package:polkadart/polkadart.dart';
import 'package:polkadart_keyring/polkadart_keyring.dart' as keyring;

void main() async {
//
// Signatories: keypairS1, keypairS2, keypairS3
final keypairS1 = await keyring.KeyPair.sr25519.fromUri('//keypairS1');
keypairS1.ss58Format = 42;

final keypairS2 = await keyring.KeyPair.sr25519.fromUri('//keypairS2');
keypairS2.ss58Format = 42;

final keypairS3 = await keyring.KeyPair.sr25519.fromUri('//keypairS3');
keypairS3.ss58Format = 42;

// Recipient: keypairR
final keypairR = await keyring.KeyPair.sr25519.fromUri('//keypairR');
keypairR.ss58Format = 42;

final provider = Provider.fromUri(Uri.parse('wss://westend-rpc.polkadot.io'));

///
/// Create and Fund Multisig
final multiSigResponse = await Multisig.createAndFundMultisig(
depositorKeyPair: keypairS1,
otherSignatoriesAddressList: [keypairS2.address, keypairS3.address],
threshold: 2,
recipientAddress: keypairR.address,
amount: BigInt.parse('711${'0' * 10}'), // 7.11 WND
provider: provider,
);

// Json for forwarding to other signatories.
final json = multiSigResponse.toJson();

{
// Assuming keypairS2 is the first signatory who is approving.
final localResponse = MultisigResponse.fromJson(json);
// Approve this call by keypairS2 and forward for further approval.
await Future.delayed(Duration(seconds: 15));
await localResponse.approveAsMulti(provider, keypairS2);
}

/**
* Although keypairS3 can call `approveAsMulti` to approve this call which will then
* wait for `keypairS1` to approve via `asMulti` call.
*
* In this case:
* The last signatory (`keypairS1`) will not be able to call `approveAsMulti` as it will throw FinalApprovalException.
*
* So, `keypairS1` will have to call `asMulti` to approve this call.
*/

{
// Assuming keypairS3 is the second signatory who is approving.
final localResponse = MultisigResponse.fromJson(json);
// Execute this call by keypairS3 approval.
await Future.delayed(Duration(seconds: 15));
await localResponse.asMulti(provider, keypairS3);
}

// // Cancel this call by keypairS1
//
// final localResponse = MultisigResponse.fromJson(json);
//
// // Cancel this call by keypairS1
// await Future.delayed(Duration(seconds: 15));
// await multiSigResponse.cancelAsMulti(provider, keypairS1);
}
4 changes: 4 additions & 0 deletions packages/polkadart/lib/apis/apis.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import 'dart:typed_data' show Uint8List;
import 'dart:async' show Future, StreamSubscription;
import 'package:convert/convert.dart' show hex;
import 'package:polkadart/polkadart.dart';
import 'dart:typed_data';

import 'package:convert/convert.dart';

part './author.dart';
part './chain.dart';
part './state.dart';
part './system.dart';
41 changes: 41 additions & 0 deletions packages/polkadart/lib/apis/chain.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
part of apis;

/// Substrate chain API
class ChainApi<P extends Provider> {
final P _provider;

ChainApi(this._provider);

/// Returns the BlockHash for the given block number.
///
/// If no block number is provided, it will return the hash of the latest block at chain height.
Future<BlockHash> getBlockHash({int? blockNumber}) async {
final List<dynamic> params = <dynamic>[];
if (blockNumber != null) {
params.add(blockNumber);
}

final response = await _provider.send('chain_getBlockHash', params);

if (response.error != null) {
throw Exception(response.error.toString());
}

return Uint8List.fromList(
hex.decode((response.result as String).substring(2)));
}

/// Get the latest block number or specific block number by BlockHash.
Future<int> getChainHeader({BlockHash? at}) async {
final List<String> params = <String>[];
if (at != null) {
params.add('0x${hex.encode(at)}');
}
final response = await _provider.send('chain_getHeader', params);

if (response.error != null) {
throw Exception(response.error.toString());
}
return int.parse(response.result['number']!);
}
}
39 changes: 39 additions & 0 deletions packages/polkadart/lib/extrinsic/abstract_payload.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'dart:typed_data';

abstract class Payload {
final Uint8List method; // Call
final int blockNumber;
final int eraPeriod; // CheckMortality
final int nonce; // CheckNonce
final dynamic tip; // ChargeTransactionPayment
final int? assetId; // ChargeAssetTxPayment

const Payload({
required this.method,
required this.blockNumber,
required this.eraPeriod,
required this.nonce,
required this.tip,
this.assetId,
});

toEncodedMap(dynamic registry);

bool usesChargeAssetTxPayment(dynamic registry) {
if (registry.getSignedExtensionTypes() is Map) {
return (registry.getSignedExtensionTypes() as Map)
.containsKey('ChargeAssetTxPayment');
}
return (registry.getSignedExtensionTypes() as List)
.contains('ChargeAssetTxPayment');
}

String maybeAssetIdEncoded(dynamic registry) {
if (usesChargeAssetTxPayment(registry)) {
// '00' and '01' refer to rust's Option variants 'None' and 'Some'.
return assetId != null ? '01${assetId!.toRadixString(16)}' : '00';
} else {
return '';
}
}
}
Loading

0 comments on commit 017cb28

Please sign in to comment.