Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add feature to fetch contract storage into dictionary #3

Merged
merged 1 commit into from
Nov 8, 2023
Merged

Conversation

meevee98
Copy link
Contributor

Included get_storage method in SmartContractTestCase and included some tests checking the storage in nep17 example

storage = await self.get_storage(
prefix=self.balance_prefix,
remove_prefix=True
)
result, _ = await self.call(
"balanceOf", [self.user1.script_hash], return_type=int
)
self.assertEqual(expected, result)
balance_key = self.user1.script_hash.to_array()
self.assertIn(balance_key, storage)
self.assertEqual(types.BigInteger(expected).to_array(), storage[balance_key])

I implemented it returning dict[bytes, bytes], but needed to deserialize the value to check it in the balance_of tests, which let me to think if it would be worth it to implement a storage wrapper with deserialize util methods and use it as the return type instead of dict[bytes, bytes].

@meevee98 meevee98 requested a review from ixje October 30, 2023 22:40
@meevee98 meevee98 self-assigned this Oct 30, 2023
Copy link
Member

@ixje ixje left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I implemented it returning dict[bytes, bytes], but needed to deserialize the value to check it in the balance_of tests, which let me to think if it would be worth it to implement a storage wrapper with deserialize util methods and use it as the return type instead of dict[bytes, bytes].

I think for the storage keys this could well work for most use-cases, however storage values can really be anything. From simple bytes to complete serialized objects (our Props - Puppet contract comes to mind that serializes a whole User class).

I was thinking to add 2 arguments to get_storage() that accept a keys_post_processor and values_post_processor function. It could look e.g. like this for a simple values

storage: dict[types.UInt160, types.BigInteger] = await self.get_storage( 
     prefix=self.balance_prefix, 
     remove_prefix=True,
     key_post_processor=lambda x: types.UInt160(data=x),
     value_post_processor=lambda x: types.BigInteger(x)
 ) 

or for more complex things you can do

def deserialize_to_user(data: bytes) -> User:
   # process the data
   return User()

storage: dict[types.UInt160, User] = await self.get_storage( 
     prefix=self.balance_prefix, 
     remove_prefix=True,
     key_post_processor=lambda x: types.UInt160(data=x),
     value_post_processor=deserialize_to_user
 ) 

Can you elaborate a bit more on how your storage wrapper idea would work? Perhaps the ideas can be combined.

@ixje
Copy link
Member

ixje commented Oct 31, 2023

fyi; I added a stdlib.binary_deserialize() method to mamba which could be used for post processing (e.g. for the Puppet contract example mentioned above).

@meevee98
Copy link
Contributor Author

meevee98 commented Nov 7, 2023

Can you elaborate a bit more on how your storage wrapper idea would work? Perhaps the ideas can be combined.

Sorry for the late response for this. I suggested a wrapper because I saw many blockchain-related wrappers being used in the project, but I actually think that your suggestion here

I was thinking to add 2 arguments to get_storage() that accept a keys_post_processor and values_post_processor function. It could look e.g. like this for a simple values

storage: dict[types.UInt160, types.BigInteger] = await self.get_storage( 
     prefix=self.balance_prefix, 
     remove_prefix=True,
     key_post_processor=lambda x: types.UInt160(data=x),
     value_post_processor=lambda x: types.BigInteger(x)
 ) 

would be much more useful and flexible for what the user may use the storage for.

Maybe we can include the simple processors (i.e. bytes to Integer, bytes to str, the default neo default serialization from bytes to array/map) in our package so the user can only import and use them. Something like:

storage: dict[types.UInt160, list] = await self.get_storage( 
     prefix=self.balance_prefix, 
     remove_prefix=True,
     key_post_processor=deserialize_as_uint160,
     value_post_processor=deserialize_as_array
 ) 

@ixje
Copy link
Member

ixje commented Nov 8, 2023

Yes we can add some utility functions e.g. a module storage containing

def as_uint160(data: bytes) -> UInt160:
   if len(data) != 20:
      raise ValueError("invalid data length")
   return UInt160(data)

def as_public_key(data: bytes) -> ECPoint:
   if len(data) == 33:
      return ECpoint.deserialize_from_bytes(data)
   elif len(data) == 66:
      # handle uncompressed
   else:
      raise ValueError("invalid public key")
  etc

and then use as

s: dict[types.UInt160, list] = await self.get_storage( 
     prefix=self.balance_prefix, 
     remove_prefix=True,
     key_post_processor=storage.as_uint160,
     value_post_processor=storage.as_public_key
 ) 

this is very similar to how the unwrap helpers work on the mamba side

@ixje ixje merged commit 79a11c7 into master Nov 8, 2023
@ixje ixje deleted the #86a163w73 branch November 8, 2023 09:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants