Skip to content

Commit

Permalink
Multisig helper functions (#245)
Browse files Browse the repository at this point in the history
  • Loading branch information
arjanz authored Sep 19, 2022
1 parent c89b728 commit f390638
Show file tree
Hide file tree
Showing 7 changed files with 721 additions and 22 deletions.
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ management and versioning of types.
* [Query a mapped storage function](#query-a-mapped-storage-function)
* [Create and send signed extrinsics](#create-and-send-signed-extrinsics)
* [Examining the ExtrinsicReceipt object](#examining-the-extrinsicreceipt-object)
* [Initiate and finalize multisig extrinsics](#initiate-and-finalize-multisig-extrinsics)
* [Type decomposition of call params](#type-decomposition-of-call-params)
* [ink! contract interfacing](#ink-contract-interfacing)
* [Create mortal extrinsics](#create-mortal-extrinsics)
Expand Down Expand Up @@ -453,6 +454,62 @@ for event in receipt.triggered_events:
print(f'* {event.value}')
```

### Initiate and finalize multisig extrinsics

To initiate and finalize multisig extrinsics, the following helper functions are available:

```python
# Define the multisig account by supplying its signatories and threshold
keypair_alice = Keypair.create_from_uri('//Alice', ss58_format=substrate.ss58_format)

multisig_account = substrate.generate_multisig_account(
signatories=[
keypair_alice.ss58_address,
'5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty',
'5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y'
],
threshold=2
)
```

Then initiate the multisig extrinsic by providing the call and a keypair of one of its signatories:

```python
call = substrate.compose_call(
call_module='System',
call_function='remark_with_event',
call_params={
'remark': 'Multisig test'
}
)

extrinsic = substrate.create_multisig_extrinsic(call, keypair_alice, multisig_account, era={'period': 64})
receipt = substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True)
```

Then a second signatory approves and finalizes the call by providing the same call to another multisig extrinsic:

```python
# Define the multisig account by supplying its signatories and threshold
keypair_charlie = Keypair.create_from_uri('//Charlie', ss58_format=substrate.ss58_format)

multisig_account = substrate.generate_multisig_account(
signatories=[
keypair_charlie.ss58_address,
'5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
'5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y'
],
threshold=2
)

extrinsic = substrate.create_multisig_extrinsic(call, keypair_charlie, multisig_account, era={'period': 64})
receipt = substrate.submit_extrinsic(extrinsic, wait_for_inclusion=True)
```

The call will be executed when the second and final multisig extrinsic is submitted, condition and state of the multig
will be checked on-chain during processing of the multisig extrinsic.


### Type decomposition of call params

To get more information how to construct the parameters of a certain call (For example to use in `substrate.compose_call()`):
Expand Down
329 changes: 317 additions & 12 deletions docs/base.html

Large diffs are not rendered by default.

238 changes: 231 additions & 7 deletions docs/index.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ eth_utils>=1.3.0,<3
pycryptodome>=3.11.0,<4
PyNaCl>=1.0.1,<2

scalecodec>=1.0.41,<2
scalecodec>=1.0.42,<2
py-sr25519-bindings>=0.1.4,<1
py-ed25519-zebra-bindings>=1.0,<2
py-bip39-bindings>=0.1.9,<1
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@
'eth_utils>=1.3.0,<3',
'pycryptodome>=3.11.0,<4',
'PyNaCl>=1.0.1,<2',
'scalecodec>=1.0.41,<2',
'scalecodec>=1.0.42,<2',
'py-sr25519-bindings>=0.1.4,<1',
'py-ed25519-zebra-bindings>=1.0,<2',
'py-bip39-bindings>=0.1.9,<1'
Expand Down
83 changes: 82 additions & 1 deletion substrateinterface/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from websocket import create_connection, WebSocketConnectionClosedException

from scalecodec.base import ScaleDecoder, ScaleBytes, RuntimeConfigurationObject, ScaleType
from scalecodec.types import GenericCall, GenericExtrinsic, Extrinsic
from scalecodec.types import GenericCall, GenericExtrinsic, Extrinsic, MultiAccountId
from scalecodec.type_registry import load_type_registry_preset
from scalecodec.updater import update_type_registries

Expand Down Expand Up @@ -1866,6 +1866,87 @@ def create_unsigned_extrinsic(self, call: GenericCall) -> GenericExtrinsic:

return extrinsic

def generate_multisig_account(self, signatories: list, threshold: int) -> MultiAccountId:
"""
Generate deterministic Multisig account with supplied signatories and threshold
Parameters
----------
signatories: List of signatories
threshold: Amount of approvals needed to execute
Returns
-------
MultiAccountId
"""

multi_sig_account = MultiAccountId.create_from_account_list(signatories, threshold)

multi_sig_account.ss58_address = ss58_encode(multi_sig_account.value.replace('0x', ''), self.ss58_format)

return multi_sig_account

def create_multisig_extrinsic(self, call: GenericCall, keypair: Keypair, multisig_account: MultiAccountId,
max_weight: Optional[int] = None, era: dict = None, nonce: int = None, tip: int = 0,
tip_asset_id: int = None, signature: Union[bytes, str] = None) -> GenericExtrinsic:
"""
Create a Multisig extrinsic that will be signed by one of the signatories. Checks on-chain if the threshold
of the multisig account is reached and try to execute the call accordingly.
Parameters
----------
call: GenericCall to create extrinsic for
keypair: Keypair of the signatory to approve given call
multisig_account: MultiAccountId to use of origin of the extrinsic (see `generate_multisig_account()`)
max_weight: Maximum allowed weight to execute the call ( Uses `get_payment_info()` by default)
era: Specify mortality in blocks in follow format: {'period': [amount_blocks]} If omitted the extrinsic is immortal
nonce: nonce to include in extrinsics, if omitted the current nonce is retrieved on-chain
tip: The tip for the block author to gain priority during network congestion
tip_asset_id: Optional asset ID with which to pay the tip
signature: Optionally provide signature if externally signed
Returns
-------
GenericExtrinsic
"""
if max_weight is None:
payment_info = self.get_payment_info(call, keypair)
# Check type of weight as per https://github.com/paritytech/substrate/pull/12138
if type(payment_info["weight"]) is dict:
max_weight = payment_info["weight"]["ref_time"]
else:
max_weight = payment_info["weight"]

# Check if call has existing approvals
multisig_details = self.query("Multisig", "Multisigs", [multisig_account.value, call.call_hash])

if multisig_details.value:
maybe_timepoint = multisig_details.value['when']
else:
maybe_timepoint = None

# Compose 'as_multi' when final, 'approve_as_multi' otherwise
if multisig_details.value and len(multisig_details.value['approvals']) + 1 == multisig_account.threshold:
multi_sig_call = self.compose_call("Multisig", "as_multi", {
'other_signatories': [s for s in multisig_account.signatories if s != f'0x{keypair.public_key.hex()}'],
'threshold': multisig_account.threshold,
'maybe_timepoint': maybe_timepoint,
'call': call,
'store_call': False,
'max_weight': max_weight
})
else:
multi_sig_call = self.compose_call("Multisig", "approve_as_multi", {
'other_signatories': [s for s in multisig_account.signatories if s != f'0x{keypair.public_key.hex()}'],
'threshold': multisig_account.threshold,
'maybe_timepoint': maybe_timepoint,
'call_hash': call.call_hash,
'max_weight': max_weight
})

return self.create_signed_extrinsic(
multi_sig_call, keypair, era=era, nonce=nonce, tip=tip, tip_asset_id=tip_asset_id, signature=signature
)

def submit_extrinsic(self, extrinsic: GenericExtrinsic, wait_for_inclusion: bool = False,
wait_for_finalization: bool = False) -> "ExtrinsicReceipt":
"""
Expand Down
32 changes: 32 additions & 0 deletions test/test_create_extrinsics.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,38 @@ def test_create_batch_extrinsic(self):
self.assertEqual('Utility', extrinsic.value['call']['call_module'])
self.assertEqual('batch', extrinsic.value['call']['call_function'])

def test_create_multisig_extrinsic(self):

call = self.polkadot_substrate.compose_call(
call_module='Balances',
call_function='transfer',
call_params={
'dest': 'EaG2CRhJWPb7qmdcJvy3LiWdh26Jreu9Dx6R1rXxPmYXoDk',
'value': 3 * 10 ** 3
}
)

keypair_alice = Keypair.create_from_uri('//Alice', ss58_format=self.polkadot_substrate.ss58_format)
keypair_bob = Keypair.create_from_uri('//Bob', ss58_format=self.polkadot_substrate.ss58_format)
keypair_charlie = Keypair.create_from_uri('//Charlie', ss58_format=self.polkadot_substrate.ss58_format)

multisig_account = self.polkadot_substrate.generate_multisig_account(
signatories=[
keypair_alice.ss58_address,
keypair_bob.ss58_address,
keypair_charlie.ss58_address
],
threshold=2
)

extrinsic = self.polkadot_substrate.create_multisig_extrinsic(call, self.keypair, multisig_account, era={'period': 64})

# Decode extrinsic again as test
extrinsic.decode(extrinsic.data)

self.assertEqual('Multisig', extrinsic.value['call']['call_module'])
self.assertEqual('approve_as_multi', extrinsic.value['call']['call_function'])

def test_create_unsigned_extrinsic(self):

call = self.kusama_substrate.compose_call(
Expand Down

0 comments on commit f390638

Please sign in to comment.