Skip to content

Commit e1fa0ed

Browse files
authored
Merge pull request #18 from opentensor/feat/thewhaleking/improve-scale-encoding
Move logic to mixin + fix tests
2 parents aa7ad25 + de9cf65 commit e1fa0ed

File tree

5 files changed

+107
-147
lines changed

5 files changed

+107
-147
lines changed

async_substrate_interface/async_substrate.py

Lines changed: 48 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,7 @@
2424
import asyncstdlib as a
2525
from bittensor_wallet.keypair import Keypair
2626
from bittensor_wallet.utils import SS58_FORMAT
27-
from bt_decode import (
28-
MetadataV15,
29-
PortableRegistry,
30-
decode as decode_by_type_string,
31-
encode as encode_by_type_string,
32-
)
27+
from bt_decode import MetadataV15, PortableRegistry, decode as decode_by_type_string
3328
from scalecodec.base import ScaleBytes, ScaleType, RuntimeConfigurationObject
3429
from scalecodec.types import (
3530
GenericCall,
@@ -805,14 +800,58 @@ async def load_registry(self):
805800
)
806801
self.registry = PortableRegistry.from_metadata_v15(self.metadata_v15)
807802

803+
async def _wait_for_registry(self, _attempt: int = 1, _retries: int = 3) -> None:
804+
async def _waiter():
805+
while self.registry is None:
806+
await asyncio.sleep(0.1)
807+
return
808+
809+
try:
810+
if not self.registry:
811+
await asyncio.wait_for(_waiter(), timeout=10)
812+
except TimeoutError:
813+
# indicates that registry was never loaded
814+
if not self._initializing:
815+
raise AttributeError(
816+
"Registry was never loaded. This did not occur during initialization, which usually indicates "
817+
"you must first initialize the AsyncSubstrateInterface object, either with "
818+
"`await AsyncSubstrateInterface.initialize()` or running with `async with`"
819+
)
820+
elif _attempt < _retries:
821+
await self.load_registry()
822+
return await self._wait_for_registry(_attempt + 1, _retries)
823+
else:
824+
raise AttributeError(
825+
"Registry was never loaded. This occurred during initialization, which usually indicates a "
826+
"connection or node error."
827+
)
828+
829+
async def encode_scale(
830+
self, type_string, value: Any, _attempt: int = 1, _retries: int = 3
831+
) -> bytes:
832+
"""
833+
Helper function to encode arbitrary data into SCALE-bytes for given RUST type_string
834+
835+
Args:
836+
type_string: the type string of the SCALE object for decoding
837+
value: value to encode
838+
_attempt: the current number of attempts to load the registry needed to encode the value
839+
_retries: the maximum number of attempts to load the registry needed to encode the value
840+
841+
Returns:
842+
encoded bytes
843+
"""
844+
await self._wait_for_registry(_attempt, _retries)
845+
return self._encode_scale(type_string, value)
846+
808847
async def decode_scale(
809848
self,
810849
type_string: str,
811850
scale_bytes: bytes,
812851
_attempt=1,
813852
_retries=3,
814853
return_scale_obj=False,
815-
) -> Any:
854+
) -> Union[ScaleObj, Any]:
816855
"""
817856
Helper function to decode arbitrary SCALE-bytes (e.g. 0x02000000) according to given RUST type_string
818857
(e.g. BlockNumber). The relevant versioning information of the type (if defined) will be applied if block_hash
@@ -828,95 +867,19 @@ async def decode_scale(
828867
Returns:
829868
Decoded object
830869
"""
831-
832-
async def _wait_for_registry():
833-
while self.registry is None:
834-
await asyncio.sleep(0.1)
835-
return
836-
837870
if scale_bytes == b"\x00":
838871
obj = None
839872
if type_string == "scale_info::0": # Is an AccountId
840873
# Decode AccountId bytes to SS58 address
841874
return bytes.fromhex(ss58_decode(scale_bytes, SS58_FORMAT))
842875
else:
843-
try:
844-
if not self.registry:
845-
await asyncio.wait_for(_wait_for_registry(), timeout=10)
846-
obj = decode_by_type_string(type_string, self.registry, scale_bytes)
847-
except TimeoutError:
848-
# indicates that registry was never loaded
849-
if not self._initializing:
850-
raise AttributeError(
851-
"Registry was never loaded. This did not occur during initialization, which usually indicates "
852-
"you must first initialize the AsyncSubstrateInterface object, either with "
853-
"`await AsyncSubstrateInterface.initialize()` or running with `async with`"
854-
)
855-
elif _attempt < _retries:
856-
await self.load_registry()
857-
return await self.decode_scale(
858-
type_string, scale_bytes, _attempt + 1
859-
)
860-
else:
861-
raise AttributeError(
862-
"Registry was never loaded. This occurred during initialization, which usually indicates a "
863-
"connection or node error."
864-
)
876+
await self._wait_for_registry(_attempt, _retries)
877+
obj = decode_by_type_string(type_string, self.registry, scale_bytes)
865878
if return_scale_obj:
866879
return ScaleObj(obj)
867880
else:
868881
return obj
869882

870-
async def encode_scale(self, type_string, value: Any) -> bytes:
871-
"""
872-
Helper function to encode arbitrary data into SCALE-bytes for given RUST type_string
873-
874-
Args:
875-
type_string: the type string of the SCALE object for decoding
876-
value: value to encode
877-
878-
Returns:
879-
encoded SCALE bytes
880-
"""
881-
if value is None:
882-
result = b"\x00"
883-
else:
884-
if type_string == "scale_info::0": # Is an AccountId
885-
# encode string into AccountId
886-
## AccountId is a composite type with one, unnamed field
887-
return bytes.fromhex(ss58_decode(value, SS58_FORMAT))
888-
889-
elif type_string == "scale_info::151": # Vec<AccountId>
890-
if not isinstance(value, (list, tuple)):
891-
value = [value]
892-
893-
# Encode length
894-
length = len(value)
895-
if length < 64:
896-
result = bytes([length << 2]) # Single byte mode
897-
else:
898-
raise ValueError("Vector length too large")
899-
900-
# Encode each AccountId
901-
for account in value:
902-
if isinstance(account, bytes):
903-
result += account # Already encoded
904-
else:
905-
result += bytes.fromhex(
906-
ss58_decode(value, SS58_FORMAT)
907-
) # SS58 string
908-
return result
909-
910-
if isinstance(value, ScaleType):
911-
if value.data.data is not None:
912-
# Already encoded
913-
return bytes(value.data.data)
914-
else:
915-
value = value.value # Unwrap the value of the type
916-
917-
result = bytes(encode_by_type_string(type_string, self.registry, value))
918-
return result
919-
920883
async def _first_initialize_runtime(self):
921884
"""
922885
TODO docstring

async_substrate_interface/sync_substrate.py

Lines changed: 3 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,7 @@
66

77
from bittensor_wallet.keypair import Keypair
88
from bittensor_wallet.utils import SS58_FORMAT
9-
from bt_decode import (
10-
MetadataV15,
11-
PortableRegistry,
12-
decode as decode_by_type_string,
13-
encode as encode_by_type_string,
14-
)
9+
from bt_decode import MetadataV15, PortableRegistry, decode as decode_by_type_string
1510
from scalecodec import (
1611
GenericCall,
1712
GenericExtrinsic,
@@ -630,56 +625,6 @@ def decode_scale(
630625
else:
631626
return obj
632627

633-
def encode_scale(self, type_string, value: Any) -> bytes:
634-
"""
635-
Helper function to encode arbitrary data into SCALE-bytes for given RUST type_string
636-
637-
Args:
638-
type_string: the type string of the SCALE object for decoding
639-
value: value to encode
640-
641-
Returns:
642-
encoded SCALE bytes
643-
"""
644-
if value is None:
645-
result = b"\x00"
646-
else:
647-
if type_string == "scale_info::0": # Is an AccountId
648-
# encode string into AccountId
649-
## AccountId is a composite type with one, unnamed field
650-
return bytes.fromhex(ss58_decode(value, SS58_FORMAT))
651-
652-
elif type_string == "scale_info::151": # Vec<AccountId>
653-
if not isinstance(value, (list, tuple)):
654-
value = [value]
655-
656-
# Encode length
657-
length = len(value)
658-
if length < 64:
659-
result = bytes([length << 2]) # Single byte mode
660-
else:
661-
raise ValueError("Vector length too large")
662-
663-
# Encode each AccountId
664-
for account in value:
665-
if isinstance(account, bytes):
666-
result += account # Already encoded
667-
else:
668-
result += bytes.fromhex(
669-
ss58_decode(value, SS58_FORMAT)
670-
) # SS58 string
671-
return result
672-
673-
if isinstance(value, ScaleType):
674-
if value.data.data is not None:
675-
# Already encoded
676-
return bytes(value.data.data)
677-
else:
678-
value = value.value # Unwrap the value of the type
679-
680-
result = bytes(encode_by_type_string(type_string, self.registry, value))
681-
return result
682-
683628
def _first_initialize_runtime(self):
684629
"""
685630
TODO docstring
@@ -2933,3 +2878,5 @@ def close(self):
29332878
self.ws.shutdown()
29342879
except AttributeError:
29352880
pass
2881+
2882+
encode_scale = SubstrateMixin._encode_scale

async_substrate_interface/types.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from datetime import datetime
77
from typing import Optional, Union, Any
88

9-
from bt_decode import PortableRegistry
9+
from bt_decode import PortableRegistry, encode as encode_by_type_string
10+
from bittensor_wallet.utils import SS58_FORMAT
1011
from scalecodec import ss58_encode, ss58_decode, is_valid_ss58_address
1112
from scalecodec.base import RuntimeConfigurationObject, ScaleBytes
1213
from scalecodec.type_registry import load_type_registry_preset
@@ -705,3 +706,53 @@ def make_payload(id_: str, method: str, params: list) -> dict:
705706
"id": id_,
706707
"payload": {"jsonrpc": "2.0", "method": method, "params": params},
707708
}
709+
710+
def _encode_scale(self, type_string, value: Any) -> bytes:
711+
"""
712+
Helper function to encode arbitrary data into SCALE-bytes for given RUST type_string
713+
714+
Args:
715+
type_string: the type string of the SCALE object for decoding
716+
value: value to encode
717+
718+
Returns:
719+
encoded bytes
720+
"""
721+
if value is None:
722+
result = b"\x00"
723+
else:
724+
if type_string == "scale_info::0": # Is an AccountId
725+
# encode string into AccountId
726+
## AccountId is a composite type with one, unnamed field
727+
return bytes.fromhex(ss58_decode(value, SS58_FORMAT))
728+
729+
elif type_string == "scale_info::151": # Vec<AccountId>
730+
if not isinstance(value, (list, tuple)):
731+
value = [value]
732+
733+
# Encode length
734+
length = len(value)
735+
if length < 64:
736+
result = bytes([length << 2]) # Single byte mode
737+
else:
738+
raise ValueError("Vector length too large")
739+
740+
# Encode each AccountId
741+
for account in value:
742+
if isinstance(account, bytes):
743+
result += account # Already encoded
744+
else:
745+
result += bytes.fromhex(
746+
ss58_decode(value, SS58_FORMAT)
747+
) # SS58 string
748+
return result
749+
750+
if isinstance(value, ScaleType):
751+
if value.data.data is not None:
752+
# Already encoded
753+
return bytes(value.data.data)
754+
else:
755+
value = value.value # Unwrap the value of the type
756+
757+
result = bytes(encode_by_type_string(type_string, self.registry, value))
758+
return result

tests/unit_tests/asyncio/test_substrate_interface.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import unittest.mock
22

33
import pytest
4-
import scalecodec.base
54
from websockets.exceptions import InvalidURI
65

76
from async_substrate_interface.async_substrate import AsyncSubstrateInterface
7+
from async_substrate_interface.types import ScaleObj
88

99

1010
@pytest.mark.asyncio
@@ -59,7 +59,7 @@ async def test_runtime_call(monkeypatch):
5959
"SubstrateMethod",
6060
)
6161

62-
assert isinstance(result, scalecodec.base.ScaleType)
62+
assert isinstance(result, ScaleObj)
6363
assert result.value is substrate.decode_scale.return_value
6464

6565
substrate.rpc_request.assert_called_once_with(

tests/unit_tests/sync/test_substrate_interface.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import unittest.mock
22

3-
import scalecodec.base
4-
53
from async_substrate_interface.sync_substrate import SubstrateInterface
4+
from async_substrate_interface.types import ScaleObj
65

76

87
def test_runtime_call(monkeypatch):
@@ -45,7 +44,7 @@ def test_runtime_call(monkeypatch):
4544
"SubstrateMethod",
4645
)
4746

48-
assert isinstance(result, scalecodec.base.ScaleType)
47+
assert isinstance(result, ScaleObj)
4948
assert result.value is substrate.decode_scale.return_value
5049

5150
substrate.rpc_request.assert_called_once_with(

0 commit comments

Comments
 (0)