Skip to content

Commit

Permalink
Creating and sending of signed extrinsics
Browse files Browse the repository at this point in the history
  • Loading branch information
Arjan Zijderveld committed May 12, 2020
1 parent 93c0ed1 commit 0fdb78e
Show file tree
Hide file tree
Showing 6 changed files with 370 additions and 30 deletions.
73 changes: 52 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,42 +78,73 @@ The modules and storage functions are provided in the metadata (see `substrate.g
parameters will be automatically converted to SCALE-bytes (also including decoding of SS58 addresses).

```python
print("\n\nCurrent balance: {} DOT".format(
substrate.get_runtime_state(
module='Balances',
storage_function='FreeBalance',
params=['EaG2CRhJWPb7qmdcJvy3LiWdh26Jreu9Dx6R1rXxPmYXoDk']
).get('result') / 10**12
))
balance_info = substrate.get_runtime_state(
module='System',
storage_function='Account',
params=['5E9oDs9PjpsBbxXxRE9uMaZZhnBAV38n2ouLB28oecBDdeQo']
).get('result')

if balance_info:
print("\n\nCurrent free balance: {} KSM".format(
balance_info.get('data').get('free', 0) / 10**12
))
```

Or get a historic balance at a certain block hash:

```python
print("Balance @ {}: {} DOT".format(
block_hash,
substrate.get_runtime_state(
module='Balances',
storage_function='FreeBalance',
params=['EaG2CRhJWPb7qmdcJvy3LiWdh26Jreu9Dx6R1rXxPmYXoDk'],
block_hash=block_hash
).get('result') / 10**12
))
balance_info = substrate.get_runtime_state(
module='System',
storage_function='Account',
params=['5E9oDs9PjpsBbxXxRE9uMaZZhnBAV38n2ouLB28oecBDdeQo'],
block_hash=block_hash
).get('result')

if balance_info:
print("\n\nFree balance @ {}: {} KSM".format(
block_hash,
balance_info.get('data').get('free', 0) / 10**12
))
```

### Compose call
### Create and send signed extrinsics

Py-substrate-interface will also let you compose calls you can use as an unsigned extrinsic or as a proposal:
The following code snippet illustrates how to create a call, wrap it in an signed extrinsic and send it to the network:

```python
payload = substrate.compose_call(
from substrateinterface import SubstrateInterface, SubstrateRequestException, Keypair

substrate = SubstrateInterface(
url="http://127.0.0.1:9933",
address_type=42,
type_registry_preset='kusama'
)

keypair = Keypair(
ss58_address='5HmubXCdmtEvKmvqjJ7fXkxhPXcg6JTS62kMMphqxpEE6zcG',
public_key='0xfc99becc4334e76e75d2e3bd3be759728b843c53954f1cada66ae9f6da97ab54',
private_key='0x8bb70006b5ca74fc1f26afaab8c65b6dc3c8fe9983c2c99880e5ffb74d1dcb09d8784e4a1befd363b34ac6bad337fa75dee8e4373914aa10d0263aefe04346dd',
suri='episode together nose spoon dose oil faculty zoo ankle evoke admit walnut'
)

call = substrate.compose_call(
call_module='Balances',
call_function='transfer',
call_params={
'dest': 'EaG2CRhJWPb7qmdcJvy3LiWdh26Jreu9Dx6R1rXxPmYXoDk',
'value': 1000000000000
'dest': '5E9oDs9PjpsBbxXxRE9uMaZZhnBAV38n2ouLB28oecBDdeQo',
'value': 1 * 10**12
}
)

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

try:
extrinsic_hash = substrate.send_extrinsic(extrinsic)
print("Extrinsic sent: {}".format(extrinsic_hash))

except SubstrateRequestException as e:
print("Failed to send: {}".format(e))

```

Py-substrate-interface makes it also possible to easily interprete changed types and historic runtimes. As an example
Expand Down
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
docker==4.2.0
asyncio==3.4.3
websockets==8.1
base58==1.0.3
Expand All @@ -9,4 +10,5 @@ urllib3==1.25.3
xxhash==1.3.0
pytest==4.4.0

scalecodec>=0.9.26
scalecodec>=0.9.42
py-sr25519-bindings>=0.1.0
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@
'requests==2.22.0',
'urllib3==1.25.3',
'xxhash==1.3.0',
'scalecodec>=0.9.26'
'scalecodec>=0.9.42',
'py-sr25519-bindings>=0.1.0'
],

# List additional groups of dependencies here (e.g. development
Expand Down
179 changes: 172 additions & 7 deletions substrateinterface/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,61 @@
from scalecodec.metadata import MetadataDecoder
from scalecodec.type_registry import load_type_registry_preset

from .subkey import Subkey
from .utils.hasher import blake2_256, two_x64_concat, xxh64, xxh128, blake2_128, blake2_128_concat, identity
from .exceptions import SubstrateRequestException
from .exceptions import SubstrateRequestException, ConfigurationError, StorageFunctionNotFound
from .constants import *
from .utils.ss58 import ss58_decode

try:
import sr25519
except ImportError:
sr25519 = None


class Keypair:

def __init__(self, ss58_address=None, public_key=None, private_key=None, suri=None):

if ss58_address:
public_key = ss58_decode(ss58_address)

if not public_key:
raise ValueError('No SS58 formatted address or public key provided')

public_key = '0x{}'.format(public_key.replace('0x', ''))

if len(public_key) != 66:
raise ValueError('Public key should be 32 bytes long')

self.public_key = public_key

if private_key:
private_key = '0x{}'.format(private_key.replace('0x', ''))

if len(private_key) != 130:
raise ValueError('Secret key should be 64 bytes long')

self.private_key = private_key

if suri:
# TODO automatically convert mnemonic to private key
pass

self.suri = suri


class SubstrateInterface:

def __init__(self, url, address_type=None, type_registry=None, type_registry_preset=None, cache_region=None):
def __init__(self, url, address_type=None, type_registry=None, type_registry_preset=None, cache_region=None,
sub_key: Subkey = None):
"""
A specialized class in interfacing with a Substrate node.
Parameters
----------
url: the URL to the substrate node, either in format https://127.0.0.1:9933 or wss://127.0.0.1:9944
address_type: : The address type which account IDs will be SS58-encoded to Substrate addresses. Defaults to 42, for Kusama the address type is 2
address_type: The address type which account IDs will be SS58-encoded to Substrate addresses. Defaults to 42, for Kusama the address type is 2
type_registry: A dict containing the custom type registry in format: {'types': {'customType': 'u32'},..}
type_registry_preset: The name of the predefined type registry shipped with the SCALE-codec, e.g. kusama
cache_region: a Dogpile cache region as a central store for the metadata cache
Expand Down Expand Up @@ -83,6 +122,8 @@ def __init__(self, url, address_type=None, type_registry=None, type_registry_pre
self.metadata_cache = {}
self.type_registry_cache = {}

self.sub_key = sub_key

self.debug = False

def debug_message(self, message):
Expand Down Expand Up @@ -665,7 +706,7 @@ def get_runtime_state(self, module, storage_function, params=None, block_hash=No

return response

raise ValueError('Storage function "{}.{}" not found'.format(module, storage_function))
raise StorageFunctionNotFound('Storage function "{}.{}" not found'.format(module, storage_function))

def get_runtime_events(self, block_hash=None):
"""
Expand Down Expand Up @@ -725,15 +766,139 @@ def compose_call(self, call_module, call_function, call_params=(), block_hash=No
"""
self.init_runtime(block_hash=block_hash)

extrinsic = ExtrinsicsDecoder(metadata=self.metadata_decoder, address_type=self.address_type)
call = ScaleDecoder.get_decoder_class('Call', metadata=self.metadata_decoder)

payload = extrinsic.encode({
call.encode({
'call_module': call_module,
'call_function': call_function,
'call_args': call_params
})

return str(payload)
return call

def get_account_nonce(self, account_address):
response = self.get_runtime_state('System', 'Account', [account_address])
if response.get('result'):
return response['result'].get('nonce', 0)

def verify_data(self, data):
pass

def sign_data(self, data, keypair: Keypair):

if sr25519:
if data[0:2] == '0x':
data = bytes.fromhex(data[2:])
else:
data = data.encode()

if not keypair.private_key:
raise ConfigurationError('private_key must be set on keypair to use sr25519 bindings')

signature = sr25519.sign(
(bytes.fromhex(keypair.public_key[2:]), bytes.fromhex(keypair.private_key[2:])),
data
)
return "0x{}".format(signature.hex())

elif self.sub_key:
if not keypair.suri:
raise ConfigurationError('suri must be set on keypair to use subkey')
return self.sub_key.sign(data, keypair.suri)

raise ConfigurationError(
"No RUST bindings available and no subkey implementation provided for signing"
)

def create_signed_extrinsic(self, call, keypair: Keypair, nonce=None, tip=0):
"""
Creates a extrinsic signed by given account details
Parameters
----------
keypair
call
nonce
tip
Returns
-------
ExtrinsicsDecoder The signed Extrinsic
"""

# Check requirements
if call.__class__.__name__ != 'Call':
raise TypeError("'call' must be of type Call")

# Retrieve nonce
if not nonce:
nonce = self.get_account_nonce(keypair.public_key) or 0

# Retrieve genesis hash
genesis_hash = self.get_block_hash(0)

# TODO implement MortalEra transactions
era = '00'

# Create signature payload
signature_payload = ScaleDecoder.get_decoder_class('ExtrinsicPayloadValue')

signature_payload.encode({
'call': str(call.data),
'era': era,
'nonce': nonce,
'tip': tip,
'specVersion': self.runtime_version,
'genesisHash': genesis_hash,
'blockHash': genesis_hash
})

# Sign payload
signature = self.sign_data(data=str(signature_payload.data), keypair=keypair)

# Create extrinsic
extrinsic = ScaleDecoder.get_decoder_class('Extrinsic', metadata=self.metadata_decoder)

extrinsic.encode({
'account_id': keypair.public_key,
'signature_version': 1,
'signature': signature,
'call_function': call.value['call_function'],
'call_module': call.value['call_module'],
'call_args': call.value['call_args'],
'nonce': nonce,
'era': '00',
'tip': 0
})

return extrinsic

def create_unsigned_extrinsic(self, call):
# TODO implement
pass

def send_extrinsic(self, extrinsic):
"""
Parameters
----------
extrinsic: ExtrinsicsDecoder The extinsic to be send to the network
Returns
-------
The hash of the extrinsic submitted to the network
"""

# Check requirements
if extrinsic.__class__.__name__ != 'ExtrinsicsDecoder':
raise TypeError("'extrinsic' must be of type ExtrinsicsDecoder")

response = self.rpc_request("author_submitExtrinsic", [str(extrinsic.data)])
if 'result' in response:
return response['result']
else:
raise SubstrateRequestException(response.get('error'))

def process_metadata_typestring(self, type_string):
"""
Expand Down
8 changes: 8 additions & 0 deletions substrateinterface/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,11 @@

class SubstrateRequestException(Exception):
pass


class StorageFunctionNotFound(ValueError):
pass


class ConfigurationError(Exception):
pass
Loading

0 comments on commit 0fdb78e

Please sign in to comment.