Skip to content

Commit

Permalink
Release 0.65.0
Browse files Browse the repository at this point in the history
  • Loading branch information
FrankC01 committed Jul 18, 2024
1 parent e893f2e commit 3f05730
Show file tree
Hide file tree
Showing 12 changed files with 306 additions and 140 deletions.
1 change: 0 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"rogalmic.bash-debug",
"ms-python.black-formatter",
"vadimcn.vscode-lldb",
"serayuzgur.crates",
"ms-vscode.makefile-tools",
"mikestead.dotenv",
"graphql.vscode-graphql-syntax",
Expand Down
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- SyncGqlClient and AsyncGqlClient as aliases in `pysui.__init__`


### Fixed

### Changed

- *BREAKING* SuiConfig is no longer used to instantiate a GraphQL client, instead
use PysuiConfiguration
- PysuiConfiguration, SyncGqlClient and AsyncGqlClient as aliases in `pysui.__init__`
- [change](https://github.com/FrankC01/pysui/issues/218) GraphQL remove SuiTransaction legacy dependency
- Deprecated Event and Transaction subscriptions and subscription Client
- Deprecate all Builders
Expand Down
27 changes: 16 additions & 11 deletions doc/source/graphql.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Simple dev example
#
"""Development example."""
from pysui import SuiConfig, handle_result, SyncGqlClient
from pysui import PysuiConfiguration, handle_result, SyncGqlClient
import pysui.sui.sui_pgql.pgql_query as qn
Expand All @@ -70,7 +70,8 @@ Simple dev example
if __name__ == "__main__":
# Initialize synchronous client
client_init = SyncGqlClient(config=SuiConfig.default_config(),write_schema=False)
cfg = PysuiConfiguration(group_name=PysuiConfiguration.SUI_GQL_RPC_GROUP )
client_init = SyncGqlClient(pysui_config=cfg,write_schema=False)
main(client_init)
------
Expand All @@ -83,13 +84,14 @@ copy of a Sui GraphQL RPC schema in use, you can instruct pysui to write a copy
.. code-block:: python
:linenos:
from pysui import SuiConfig, SyncGqlClient
from pysui import PysuiConfiguration, SyncGqlClient
def main():
"""Dump Sui GraphQL Schema."""
# Initialize synchronous client
client_init = SyncGqlClient(config=SuiConfig.default_config(),write_schema=True)
print(f"Schema dumped to: {client_init.schema_version}.graqhql`")
cfg = PysuiConfiguration(group_name=PysuiConfiguration.SUI_GQL_RPC_GROUP )
client_init = SyncGqlClient(pysui_config=cfg,write_schema=True)
print(f"Schema dumped to: {client_init.base_schema_version}.graqhql`")
if __name__ == "__main__":
main()
Expand All @@ -106,13 +108,14 @@ If not provided at construction, it defaults to ``{"headers":None}``
.. code-block:: python
:emphasize-lines: 8,15
from pysui import SuiConfig, SyncGqlClient
from pysui import PysuiConfiguration, SyncGqlClient
import pysui.sui.sui_pgql.pgql_query as qn
def main():
"""Set global headers to include in the RPC calls."""
# Initialize synchronous client with default headers
client = SyncGqlClient(config=SuiConfig.default_config(),default_header={"headers": {"from": "[email protected]"}})
cfg = PysuiConfiguration(group_name=PysuiConfiguration.SUI_GQL_RPC_GROUP )
client_init = SyncGqlClient(pysui_config=cfg,default_header={"headers": {"from": "[email protected]"}})
print(client.client_headers)
# Use different 'from' in headers for this one call
qres = client.execute_query_node(
Expand Down Expand Up @@ -182,7 +185,7 @@ convert the sting to a ``DocumentNode``, execute the query and either return the
#
"""String query example."""
from pysui import SuiConfig, SyncGqlClient
from pysui import PysuiConfiguration, SyncGqlClient
def main(client: SyncGqlClient):
"""Execute a static string query."""
Expand All @@ -202,7 +205,8 @@ convert the sting to a ``DocumentNode``, execute the query and either return the
if __name__ == "__main__":
# Initialize synchronous client
client_init = SyncGqlClient(config=SuiConfig.default_config(),write_schema=False)
cfg = PysuiConfiguration(group_name=PysuiConfiguration.SUI_GQL_RPC_GROUP )
client_init = SyncGqlClient(pysui_config=cfg)
main(client_init)
-----------------------
Expand All @@ -218,7 +222,7 @@ using ``gql`` functions.
"""DocumentNode query example."""
from gql import gql
from pysui import SuiConfig, SyncGqlClient
from pysui import PysuiConfiguration, SyncGqlClient
def main(client: SyncGqlClient):
"""Execute a DocumentNode as result of `gql` compilation."""
Expand All @@ -228,7 +232,8 @@ using ``gql`` functions.
if __name__ == "__main__":
# Initialize synchronous client
client_init = SyncGqlClient(config=SuiConfig.default_config(),write_schema=False)
cfg = PysuiConfiguration(group_name=PysuiConfiguration.SUI_GQL_RPC_GROUP )
client_init = SyncGqlClient(pysui_config=cfg)
main(client_init)
-----------------------
Expand Down
5 changes: 3 additions & 2 deletions doc/source/graphql_prog_txn.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ New
.. code-block:: python
:linenos:
from pysui import SuiConfig, SyncGqlClient
from pysui import PysuiConfiguration, SyncGqlClient
from pysui.sui.sui_pgql.pgql_sync_txn import SuiTransaction
client = SyncGqlClient(config=SuiConfig.default_config()) # Keyword argument
cfg = PysuiConfiguration(group_name=PysuiConfiguration.SUI_GQL_RPC_GROUP )
client = SyncGqlClient(pysui_config=cfg)
txn = SuiTransaction(client=client)
===============
Expand Down
75 changes: 73 additions & 2 deletions doc/source/graphql_pyconfig.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,79 @@
GraphQL PysuiConfiguration
""""""""""""""""""""""""""""""""
""""""""""""""""""""""""""

PysuiConfiguration is the replacement for the legacy SuiConfig and this new class should be
used with GraphQL clients.
used when creating a new GraphQL clients. This new configuration scheme is also leveraged
by the GraphQL SuiTransaction.


General
=======
Up until now, pysui relied on the presence of ``~/.sui`` and it's constituent configuration elements including
``client.yaml``, ``sui.keystore``, and ``sui.aliases`` which were all encapsulated by SuiConfig. While SuiConfig
supported some maniplations (i.e. adding new keys, alias management, etc.) it fell short of a more robust management
strategy. In addition, the code itself did not gracefully age with the advent of Sui GraphQL RPC. Until Mysten
eliminates JSON RPC, SuiConfig may continue to be used with the JSON RPC clients.

PysuiConfiguration persists its own configuratoin (default to ``~/.pysui``) and offers more flexibility when it
comes to configuration management. Amongst other things:

#. It does not presume it's configuration is persisted to a fixed location (configurable)
#. It supports programmatic switching between it's primary components (see Anatomy below)
#. It has a smaller code base that, when legacy JSON RPC support is removed, has a smaller memory footprint
#. And more...

Anatomy of PysuiConfiguration
=============================
The primary data model for PysuiConfiguration is a series of related ``dataclasses`` objects:

* The root data model is ``PysuiConfigModel`` which is a member of PysuiConfiguration.
It manages one or more...

* ``ProfileGroup``, or group for short, encapsulates unique environment configurations.
Example configuration may include "sui_json_config" (reserved), "sui_gql_config" (reserved)
or "user" (reserved). New groups may be created by the developer. Its construct includes,
all of which are configurable:

* One or more ``Profile``, or profile, a named object containing individual urls for communicatinhg to Sui with.
* Associated keys, aliases and addresses
* Identifies an active profile
* Identifies an active address

PysuiConfiguration
==================

PysuiConfiguration is the primary object to interact with when managing or using the underlying groups and
profiles. However; it is import to understand the initial setup before using it as part of the GraphQL Clients
and the arguments you can pass to the constructor rely on that understanding.

First time instantiation
------------------------
The first time PysuiConfiguration is used it will look for the ``PysuiConfig.json`` file in the
default configuration folder ``~/.pysui`` unless that folder path is overriden with
the argument ``from_cfg_path``. If the configuration file is found it is loaded, otherwise it initializes
a new configuration file with initial groups and their contents.

If Sui binaries installed
~~~~~~~~~~~~~~~~~~~~~~~~~
Three groups will be created and initially populated:

**sui_json_config** - This group and it's profiles will be populated from the contents of ``~/.sui/sui_config`` from the
files ``client.yaml``, ``sui.keystore`` and ``sui.aliases``.

**sui_gql_config** - This group and will create four profiles, each one with a ``url`` pointing to Mysten's free
GraphQL nodes. It will also copy the keys, aliases and addresses from the ``sui_json_config`` group.

**user** - Even though you may create your own groups this group is initially created as a convenience and begins
empty.

The location of the ``sui`` binary will be captured, if present, enabling Move project compiling.

If Sui binaries not installed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**sui_gql_config** and **user** will be created however; **sui_gql_config** will be populated with a new
active address and keypair (ed25519), an alias of 'Primary', and will make 'testnet' profile active.

The location of the ``sui`` binary will be captured, if present, enabling Move project compiling.

PysuiConfiguration Method Summaries
===================================
6 changes: 4 additions & 2 deletions pgql_s_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,9 @@ def do_unstake(client: SyncGqlClient):
if __name__ == "__main__":

#
cfg = PysuiConfiguration(group_name=PysuiConfiguration.SUI_GQL_RPC_GROUP)
cfg = PysuiConfiguration(
group_name=PysuiConfiguration.SUI_GQL_RPC_GROUP # , profile_name="testnet"
)
client_init = SyncGqlClient(
write_schema=False,
pysui_config=cfg,
Expand All @@ -721,7 +723,7 @@ def do_unstake(client: SyncGqlClient):
## QueryNodes (fetch)
# do_coin_meta(client_init)
# do_coins_for_type(client_init)
do_gas(client_init)
# do_gas(client_init)
# do_all_gas(client_init)
# do_gas_ids(client_init)
# do_sysstate(client_init)
Expand Down
133 changes: 108 additions & 25 deletions pysui/sui/sui_pgql/config/confgroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
import dataclasses
from typing import Optional, Union
import dataclasses_json
from pysui.abstracts.client_keypair import SignatureScheme
import pysui.sui.sui_crypto as crypto
import pysui.sui.sui_utils as utils
from pysui.sui.sui_constants import SUI_MAX_ALIAS_LEN, SUI_MIN_ALIAS_LEN


@dataclasses.dataclass
Expand Down Expand Up @@ -164,43 +167,123 @@ def address_keypair(self, *, address: str) -> crypto.SuiKeyPair:
self.key_list[self.address_list.index(_res)].private_key_base64
)

@staticmethod
def _alias_check_or_gen(
*,
aliases: Optional[list[str]] = None,
word_counts: Optional[int] = 12,
alias: Optional[str] = None,
current_iter: Optional[int] = 0,
) -> str:
"""_alias_check_or_gen If alias is provided, checks if unique otherwise creates one or more.
:param aliases: List of existing aliases, defaults to None
:type aliases: list[str], optional
:param word_counts: Words count used for mnemonic phrase, defaults to 12
:type word_counts: Optional[int], optional
:param alias: An inbound alias, defaults to None
:type alias: Optional[str], optional
:param current_iter: Internal recursion count, defaults to 0
:type current_iter: Optional[int], optional
:return: An aliases
:rtype: str
"""
if not alias:
parts = list(
utils.partition(
crypto.gen_mnemonic_phrase(word_counts).split(" "),
int(word_counts / 2),
)
)
alias_names = [k + "-" + v for k, v in zip(*parts)]

# alias_names = self._alias_gen_batch(word_counts=word_counts)
# Find a unique part if just_one
if not aliases:
alias = alias_names[0]
else:
for alias_name in alias_names:
if alias_name not in aliases:
# Found one
alias = alias_name
break
# If all match (unlikely), try unless threshold
if not alias:
if current_iter > 2:
raise ValueError("Unable to find unique alias")
else:
alias = ProfileGroup._alias_check_or_gen(
aliases=aliases,
word_counts=word_counts,
current_iter=current_iter + 1,
)
else:
if alias in aliases:
raise ValueError(f"Alias {alias} already exists.")
if not (SUI_MIN_ALIAS_LEN <= len(alias) <= SUI_MAX_ALIAS_LEN):
raise ValueError(
f"Invalid alias string length, must be betwee {SUI_MIN_ALIAS_LEN} and {SUI_MAX_ALIAS_LEN} characters."
)
return alias

@staticmethod
def new_keypair_parts(
*,
of_keytype: Optional[SignatureScheme] = SignatureScheme.ED25519,
word_counts: Optional[int] = 12,
derivation_path: Optional[str] = None,
alias: Optional[str] = None,
alias_list: list[ProfileAlias],
) -> tuple[str, str, ProfileKey, ProfileAlias]:
"""."""
mnem, keypair = crypto.create_new_keypair(
scheme=of_keytype,
word_counts=word_counts,
derv_path=derivation_path,
)
_new_keystr = keypair.serialize()
_new_prf_key = ProfileKey(_new_keystr)
# Generate artifacts
_pkey_bytes = keypair.to_bytes()
_digest = _pkey_bytes[0:33] if _pkey_bytes[0] == 0 else _pkey_bytes[0:34]
# ProfileAlias Entry
if not alias:
alias = ProfileGroup._alias_check_or_gen(
aliases=alias_list, alias=alias, word_counts=word_counts
)

_new_alias = ProfileAlias(
alias,
base64.b64encode(keypair.public_key.scheme_and_key()).decode(),
)
_new_addy = format(f"0x{hashlib.blake2b(_digest, digest_size=32).hexdigest()}")
return mnem, _new_addy, _new_prf_key, _new_alias

def add_keypair_and_parts(
self,
*,
new_alias: str,
new_keypair: crypto.SuiKeyPair,
new_address: str,
new_alias: ProfileAlias,
new_key: ProfileKey,
make_active: Optional[bool] = False,
) -> str:
"""Add a new keypair with associated address and alias."""
_new_keystr = new_keypair.serialize()
if not self._key_exists(key_string=_new_keystr):
if self._alias_exists(alias_name=new_alias):

if not self._key_exists(key_string=new_key.private_key_base64):
if self._alias_exists(alias_name=new_alias.alias):
raise ValueError(
f"Alias {new_alias} already exist attempting new key and address."
f"Alias {new_alias.alias} already exist attempting new key and address."
)
# ProfileKey Entry
_prvk_entry = ProfileKey(_new_keystr)
# Generate artifacts
_pkey_bytes = new_keypair.to_bytes()
_digest = _pkey_bytes[0:33] if _pkey_bytes[0] == 0 else _pkey_bytes[0:34]
# ProfileAlias Entry
_alias_entry = ProfileAlias(
new_alias,
base64.b64encode(new_keypair.public_key.scheme_and_key()).decode(),
)
_new_addy = format(
f"0x{hashlib.blake2b(_digest, digest_size=32).hexdigest()}"
)
# Populate group
self.address_list.append(_new_addy)
self.key_list.append(_prvk_entry)
self.alias_list.append(_alias_entry)
self.address_list.append(new_address)
self.key_list.append(new_key)
self.alias_list.append(new_alias)

if make_active:
self.using_address = _new_addy
return _new_addy
self.using_address = new_address

raise ValueError(
f"Private keystring {_new_keystr} already exists attempting new key and address.."
f"Private keystring {new_key.private_key_base64} already exists attempting new key and address.."
)

def add_profile(self, *, new_prf: Profile, make_active: bool = False):
Expand Down
Loading

0 comments on commit 3f05730

Please sign in to comment.