Skip to content

Commit

Permalink
Added signed extensions support (#163)
Browse files Browse the repository at this point in the history
  • Loading branch information
arjanz authored Dec 2, 2021
1 parent 94f438a commit 5df61fc
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 22 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ To access these nested structures you can access those formally using:

As a convenient shorthand you can also use:

`account_info['data']['free']`
`account_info['data']['free']

`ScaleType` objects can also be automatically converted to an iterable, so if the object
is for example the `others` in the result Struct of `Staking.eraStakers` can be iterated via:
Expand Down Expand Up @@ -321,7 +321,13 @@ So the whole result of `account_info.serialize()` will be a `dict` containing th

#### Comparing values with `ScaleType` objects

[TODO]
It is possible to compare ScaleType objects directly to Python primitives, internally the serialized `value` attribute
is compared:

```python
metadata_obj[1][1]['extrinsic']['version'] # '<U8(value=4)>'
metadata_obj[1][1]['extrinsic']['version'] == 4 # True
```

### Storage subscriptions

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ eth-keys~=0.3.3
eth_utils~=1.10.0
pycryptodome>=3.11.0,<4

scalecodec>=1.0.19,<2
scalecodec>=1.0.23,<2
py-sr25519-bindings~=0.1.2
py-ed25519-bindings~=0.1.2
py-bip39-bindings~=0.1.6
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
'eth-keys>=0.3.3,<1',
'eth_utils>=1.10.0,<2',
'pycryptodome>=3.11.0,<4',
'scalecodec>=1.0.19,<2',
'scalecodec>=1.0.23,<2',
'py-sr25519-bindings~=0.1.2',
'py-ed25519-bindings~=0.1.2',
'py-bip39-bindings~=0.1.6'
Expand Down
87 changes: 83 additions & 4 deletions substrateinterface/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1422,6 +1422,22 @@ def create_scale_object(self, type_string: str, data=None, block_hash=None, **kw

return self.runtime_config.create_scale_object(type_string, data=data, **kwargs)

def create_scale_object(self, type_string: str, data=None, block_hash=None, **kwargs) -> 'ScaleType':
"""
Create a SCALE object of type `type_string`
:param type_string:
:param data:
:param block_hash: Optional block hash for moment of decoding, when omitted the chain tip will be used
:param kwargs:
:return: ScaleType
"""
self.init_runtime(block_hash=block_hash)

if 'metadata' not in kwargs:
kwargs['metadata'] = self.metadata_decoder

return self.runtime_config.create_scale_object(type_string, data=data, **kwargs)

def compose_call(self, call_module: str, call_function: str, call_params: dict = None, block_hash: str = None):
"""
Composes a call payload which can be used as an unsigned extrinsic or a proposal.
Expand Down Expand Up @@ -1494,6 +1510,60 @@ def generate_signature_payload(self, call, era=None, nonce=0, tip=0, include_cal
# Create signature payload
signature_payload = self.runtime_config.create_scale_object('ExtrinsicPayloadValue')

# Process signed extensions in metadata
if 'signed_extensions' in self.metadata_decoder[1][1]['extrinsic']:

# Base signature payload
signature_payload.type_mapping = [['call', 'CallBytes']]

# Add signed extensions to payload
signed_extensions = self.metadata_decoder.get_signed_extensions()

if 'CheckMortality' in signed_extensions:
signature_payload.type_mapping.append(
['era', signed_extensions['CheckMortality']['extrinsic']]
)

if 'CheckEra' in signed_extensions:
signature_payload.type_mapping.append(
['era', signed_extensions['CheckEra']['extrinsic']]
)

if 'CheckNonce' in signed_extensions:
signature_payload.type_mapping.append(
['nonce', signed_extensions['CheckNonce']['extrinsic']]
)

if 'ChargeTransactionPayment' in signed_extensions:
signature_payload.type_mapping.append(
['tip', signed_extensions['ChargeTransactionPayment']['extrinsic']]
)

if 'CheckSpecVersion' in signed_extensions:
signature_payload.type_mapping.append(
['spec_version', signed_extensions['CheckSpecVersion']['additional_signed']]
)

if 'CheckTxVersion' in signed_extensions:
signature_payload.type_mapping.append(
['transaction_version', signed_extensions['CheckTxVersion']['additional_signed']]
)

if 'CheckGenesis' in signed_extensions:
signature_payload.type_mapping.append(
['genesis_hash', signed_extensions['CheckGenesis']['additional_signed']]
)

if 'CheckMortality' in signed_extensions:
signature_payload.type_mapping.append(
['block_hash', signed_extensions['CheckMortality']['additional_signed']]
)

if 'CheckEra' in signed_extensions:
signature_payload.type_mapping.append(
['block_hash', signed_extensions['CheckEra']['additional_signed']]
)

if include_call_length:

length_obj = self.runtime_config.get_decoder_class('Bytes')
Expand All @@ -1509,12 +1579,10 @@ def generate_signature_payload(self, call, era=None, nonce=0, tip=0, include_cal
'tip': tip,
'spec_version': self.runtime_version,
'genesis_hash': genesis_hash,
'block_hash': block_hash
'block_hash': block_hash,
'transaction_version': self.transaction_version
}

if self.transaction_version is not None:
payload_dict['transaction_version'] = self.transaction_version

signature_payload.encode(payload_dict)

if signature_payload.data.length > 256:
Expand All @@ -1541,10 +1609,18 @@ def create_signed_extrinsic(self, call: GenericCall, keypair: Keypair, era: dict
GenericExtrinsic The signed Extrinsic
"""

self.init_runtime()

# Check requirements
if not isinstance(call, GenericCall):
raise TypeError("'call' must be of type Call")

# Check if extrinsic version is supported
if self.metadata_decoder[1][1]['extrinsic']['version'] != 4:
raise NotImplementedError(
f"Extrinsic version {self.metadata_decoder[1][1]['extrinsic']['version']} not supported"
)

# Retrieve nonce
if nonce is None:
nonce = self.get_account_nonce(keypair.ss58_address) or 0
Expand Down Expand Up @@ -1613,6 +1689,9 @@ def create_unsigned_extrinsic(self, call: GenericCall) -> GenericExtrinsic:
-------
GenericExtrinsic
"""

self.init_runtime()

# Create extrinsic
extrinsic = self.runtime_config.create_scale_object(type_string='Extrinsic', metadata=self.metadata_decoder)

Expand Down
3 changes: 2 additions & 1 deletion test/fixtures/metadata_hex.json

Large diffs are not rendered by default.

89 changes: 79 additions & 10 deletions test/test_create_extrinsics.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import unittest

from scalecodec.type_registry import load_type_registry_preset
from scalecodec import ScaleBytes
from scalecodec.type_registry import load_type_registry_file
from substrateinterface import SubstrateInterface, Keypair, ExtrinsicReceipt
from substrateinterface.exceptions import SubstrateRequestException
from test import settings
Expand All @@ -38,10 +39,73 @@ def setUpClass(cls):
type_registry_preset='polkadot'
)

def test_create_balance_transfer(self):
cls.substrate_v13 = SubstrateInterface(
url=settings.POLKADOT_NODE_URL,
ss58_format=0,
type_registry_preset='polkadot'
)

module_path = os.path.dirname(__file__)
cls.metadata_fixture_dict = load_type_registry_file(
os.path.join(module_path, 'fixtures', 'metadata_hex.json')
)

cls.metadata_v13_obj = cls.substrate_v13.runtime_config.create_scale_object(
'MetadataVersioned', data=ScaleBytes(cls.metadata_fixture_dict['V13'])
)
cls.metadata_v13_obj.decode()
cls.substrate_v13.init_runtime()
cls.substrate_v13.metadata_decoder = cls.metadata_v13_obj

# Create new keypair
mnemonic = Keypair.generate_mnemonic()
keypair = Keypair.create_from_mnemonic(mnemonic, ss58_format=2)
cls.keypair = Keypair.create_from_mnemonic(mnemonic)

def test_create_extrinsic_metadata_v13(self):

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

extrinsic = self.substrate_v13.create_signed_extrinsic(call=call, keypair=self.keypair, tip=1)

decoded_extrinsic = self.substrate_v13.create_scale_object("Extrinsic")
decoded_extrinsic.decode(extrinsic.data)

self.assertEqual(decoded_extrinsic['call']['call_module'].name, 'Balances')
self.assertEqual(decoded_extrinsic['call']['call_function'].name, 'transfer')
self.assertEqual(extrinsic['nonce'], 0)
self.assertEqual(extrinsic['tip'], 1)

def test_create_extrinsic_metadata_v14(self):

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

extrinsic = self.kusama_substrate.create_signed_extrinsic(call=call, keypair=self.keypair, tip=1)

decoded_extrinsic = self.kusama_substrate.create_scale_object("Extrinsic")
decoded_extrinsic.decode(extrinsic.data)

self.assertEqual(decoded_extrinsic['call']['call_module'].name, 'Balances')
self.assertEqual(decoded_extrinsic['call']['call_function'].name, 'transfer')
self.assertEqual(extrinsic['nonce'], 0)
self.assertEqual(extrinsic['tip'], 1)

def test_create_balance_transfer(self):

for substrate in [self.kusama_substrate, self.polkadot_substrate]:

Expand All @@ -55,9 +119,9 @@ def test_create_balance_transfer(self):
}
)

extrinsic = substrate.create_signed_extrinsic(call=call, keypair=keypair)
extrinsic = substrate.create_signed_extrinsic(call=call, keypair=self.keypair)

self.assertEqual(extrinsic['address'].value, f'0x{keypair.public_key.hex()}')
self.assertEqual(extrinsic['address'].value, f'0x{self.keypair.public_key.hex()}')
self.assertEqual(extrinsic['call']['call_module'].name, 'Balances')
self.assertEqual(extrinsic['call']['call_function'].name, 'transfer')

Expand All @@ -74,9 +138,6 @@ def test_create_balance_transfer(self):
pass

def test_create_mortal_extrinsic(self):
# Create new keypair
mnemonic = Keypair.generate_mnemonic()
keypair = Keypair.create_from_mnemonic(mnemonic, ss58_format=2)

for substrate in [self.kusama_substrate, self.polkadot_substrate]:

Expand All @@ -90,7 +151,7 @@ def test_create_mortal_extrinsic(self):
}
)

extrinsic = substrate.create_signed_extrinsic(call=call, keypair=keypair, era={'period': 64})
extrinsic = substrate.create_signed_extrinsic(call=call, keypair=self.keypair, era={'period': 64})

try:
substrate.submit_extrinsic(extrinsic)
Expand Down Expand Up @@ -170,6 +231,14 @@ def test_check_extrinsic_receipt(self):

self.assertTrue(result.is_success)

result = ExtrinsicReceipt(
substrate=self.kusama_substrate,
extrinsic_hash="0x43ef739a8e4782e306908e710f333e65843fb35a57ec2a19df21cdc12258fbd8",
block_hash="0x8ab60dacd8535d948a755f72a9e09274d17f00693bbbdb55fa898db60a9ce580"
)

self.assertTrue(result.is_success)

def test_check_extrinsic_failed_result(self):
result = ExtrinsicReceipt(
substrate=self.kusama_substrate,
Expand Down
3 changes: 0 additions & 3 deletions test/test_type_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,6 @@ def setUpClass(cls):
cls.metadata_fixture_dict = load_type_registry_file(
os.path.join(module_path, 'fixtures', 'metadata_hex.json')
)
cls.metadata_fixture_dict = load_type_registry_file(
os.path.join(module_path, 'fixtures', 'metadata_hex.json')
)
cls.runtime_config = RuntimeConfigurationObject(implements_scale_info=True)
cls.runtime_config.update_type_registry(load_type_registry_preset("metadata_types"))

Expand Down

0 comments on commit 5df61fc

Please sign in to comment.