Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e28bf7a
Problem: cmd flag don't support multiple chains (#141)
mmsqe Dec 5, 2024
8b4197d
Problem: no way to set coin type after global config get removed (#142)
mmsqe Dec 20, 2024
cc5a0ec
Problem: debug-addr rename to debug-listen-addr in relayer (#143)
mmsqe Jan 16, 2025
d4f8248
Problem: query ibc-transfer denom-trace replaced by denom (#144)
mmsqe Jan 20, 2025
5ed0026
Problem: event-query-tx-for not compatible with chain-main (#145)
Feb 5, 2025
1f29855
Problem: convert script isn't being used (#146)
mmsqe Apr 15, 2025
165e8b5
Problem: no suppport when query exposed by the external community poo…
mmsqe May 5, 2025
55c0ff0
Bump setuptools from 70.0.0 to 78.1.1 (#148)
dependabot[bot] May 21, 2025
3c5bd1d
fix: avoid key err when no app-config provided
mmsqe Jun 12, 2025
595d7d2
Merge remote-tracking branch 'origin/main' into app_config
mmsqe Jun 12, 2025
1392c1c
align coin_type
mmsqe Jul 8, 2025
a8b7c6c
feat: add 'fees' to optional fields in init_devnet
Jul 29, 2025
b143725
Problem: tendermint sub cmd is not supported
mmsqe Aug 13, 2025
dba7804
feat: enhance init_devnet to support additional optional fields and v…
allthatjazzleo Aug 14, 2025
0605611
Problem: don't support raw json config (#6)
yihuang Aug 27, 2025
f016c16
Problem: client_config only get overwritten per validator
mmsqe Sep 4, 2025
2f6a51c
Problem: client id is not refreshed based on config
mmsqe Sep 4, 2025
5537ab7
Merge pull request #7 from MANTRA-Chain/client_id
mmsqe Sep 4, 2025
0876f18
Problem: skip unnecessary has_event_query_tx_for check
mmsqe Oct 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[flake8]
max-line-length = 88
extend-ignore = E203
exclude = .git,__pycache__,./integration_tests/contracts,./integration_tests/**/*_pb2.py
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
- [#129](https://github.com/crypto-com/pystarport/pull/129) create and get validator are incompatible.
- [#137](https://github.com/crypto-com/pystarport/pull/137) support ica and icaauth cmd.
- [#139](https://github.com/crypto-com/pystarport/pull/139) support ibc channel upgrade related methods.
- [#141](https://github.com/crypto-com/pystarport/pull/141) make cmd flag support multiple chains.
- [#142](https://github.com/crypto-com/pystarport/pull/142) add coin type when create account.
- [#145](https://github.com/crypto-com/pystarport/pull/145) Backward compatible with binary that don't have event-query-tx-for.
- [#147](https://github.com/crypto-com/pystarport/pull/147) suppport query exposed by the external community pool.

*Feb 7, 2023*

Expand Down
85 changes: 72 additions & 13 deletions poetry.lock

Large diffs are not rendered by default.

105 changes: 72 additions & 33 deletions pystarport/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def __init__(
self,
data,
chain_id="chainmaind",
data_dir=None,
cmd=None,
zemu_address=ZEMU_HOST,
zemu_button_port=ZEMU_BUTTON_PORT,
Expand All @@ -57,7 +58,7 @@ def __init__(
self.zemu_address = zemu_address
self.zemu_button_port = zemu_button_port
self.chain_id = chain_id
self.data_dir = data / self.chain_id
self.data_dir = data / (data_dir if data_dir else chain_id)
self.config = json.load((self.data_dir / "config.json").open())
self.cmd = cmd or self.config.get("cmd") or CHAIN

Expand Down Expand Up @@ -186,6 +187,7 @@ def create_node(
statesync=False,
mnemonic=None,
broadcastmode="sync",
coin_type=None,
):
"""create new node in the data directory,
process information is written into supervisor config
Expand Down Expand Up @@ -257,7 +259,7 @@ def custom_edit_tm(doc):
edit_app_cfg(home / "config/app.toml", base_port, {})

# create validator account
self.create_account("validator", i, mnemonic)
self.create_account("validator", i, mnemonic, coin_type=coin_type)

# add process config into supervisor
path = self.data_dir / SUPERVISOR_CONFIG_FILE
Expand Down Expand Up @@ -311,13 +313,13 @@ def delete_account(self, name, i=0):
"delete account in i-th node's keyring"
return self.cosmos_cli(i).delete_account(name)

def create_account(self, name, i=0, mnemonic=None):
def create_account(self, name, i=0, mnemonic=None, **kwargs):
"create new keypair in i-th node's keyring"
return self.cosmos_cli(i).create_account(name, mnemonic)
return self.cosmos_cli(i).create_account(name, mnemonic, **kwargs)

def create_account_ledger(self, name, i=0):
def create_account_ledger(self, name, i=0, **kwargs):
"create new ledger keypair"
return self.cosmos_cli(i).create_account_ledger(name)
return self.cosmos_cli(i).create_account_ledger(name, **kwargs)

def init(self, i):
"the i-th node's config is already added"
Expand Down Expand Up @@ -368,8 +370,8 @@ def query_all_txs(self, addr, i=0):
def distribution_commission(self, addr, i=0):
return self.cosmos_cli(i).distribution_commission(addr)

def distribution_community(self, i=0):
return self.cosmos_cli(i).distribution_community()
def distribution_community(self, i=0, **kwargs):
return self.cosmos_cli(i).distribution_community(**kwargs)

def distribution_reward(self, delegator_addr, i=0):
return self.cosmos_cli(i).distribution_reward(delegator_addr)
Expand Down Expand Up @@ -510,8 +512,8 @@ def withdraw_all_rewards(self, from_delegator, i=0, event_query_tx=True, **kwarg
**kwargs,
)

def make_multisig(self, name, signer1, signer2, i=0):
return self.cosmos_cli(i).make_multisig(name, signer1, signer2)
def make_multisig(self, name, signer1, signer2, i=0, **kwargs):
return self.cosmos_cli(i).make_multisig(name, signer1, signer2, **kwargs)

def sign_multisig_tx(self, tx_file, multi_addr, signer_name, i=0, **kwargs):
return self.cosmos_cli(i).sign_multisig_tx(
Expand Down Expand Up @@ -834,6 +836,9 @@ def pay_packet_fee(self, port_id, channel_id, packet_seq, i=0, **kwargs):
def ibc_denom_trace(self, path, node, i=0):
return self.cosmos_cli(i).ibc_denom_trace(path, node)

def ibc_denom(self, path, node, i=0):
return self.cosmos_cli(i).ibc_denom(path, node)


def start_cluster(data_dir):
cmd = [
Expand Down Expand Up @@ -900,14 +905,17 @@ def init_devnet(
"""

def create_account(cli, account, use_ledger=False):
coin_type = account.get("coin-type")
if use_ledger:
acct = cli.create_account_ledger(account["name"])
acct = cli.create_account_ledger(account["name"], coin_type=coin_type)
elif account.get("address"):
# if address field exists, will use account with that address directly
acct = {"name": account.get("name"), "address": account.get("address")}
else:
mnemonic = account.get("mnemonic")
acct = cli.create_account(account["name"], mnemonic=mnemonic)
acct = cli.create_account(
account["name"], mnemonic=mnemonic, coin_type=coin_type
)
if mnemonic:
acct["mnemonic"] = mnemonic
vesting = account.get("vesting")
Expand Down Expand Up @@ -976,6 +984,7 @@ def create_account(cli, account, use_ledger=False):
genesis_bytes = (data_dir / "node0/config/genesis.json").read_bytes()
(data_dir / "genesis.json").write_bytes(genesis_bytes)
(data_dir / "gentx").mkdir()
chain_id = config["chain_id"]
for i, val in enumerate(config["validators"]):
src = data_dir / f"node{i}/config/genesis.json"
src.unlink()
Expand All @@ -984,23 +993,23 @@ def create_account(cli, account, use_ledger=False):

# write client config
rpc_port = ports.rpc_port(val["base_port"])
merged = jsonmerge.merge(
{
"chain-id": chain_id,
"keyring-backend": "test",
"output": "json",
"node": f"tcp://{val['hostname']}:{rpc_port}",
"broadcast-mode": "sync",
},
jsonmerge.merge(config.get("client_config", {}), val.get("client_config", {})),
)
chain_id = merged["chain-id"]
(data_dir / f"node{i}/config/client.toml").write_text(
tomlkit.dumps(
jsonmerge.merge(
{
"chain-id": config["chain_id"],
"keyring-backend": "test",
"output": "json",
"node": f"tcp://{val['hostname']}:{rpc_port}",
"broadcast-mode": "sync",
},
val.get("client_config", {}),
)
)
tomlkit.dumps(merged)
)

# now we can create ClusterCLI
cli = ClusterCLI(data_dir.parent, chain_id=config["chain_id"], cmd=cmd)
cli = ClusterCLI(data_dir.parent, chain_id=chain_id, data_dir=config["chain_id"], cmd=cmd)

# patch the genesis file
genesis = jsonmerge.merge(
Expand All @@ -1013,30 +1022,45 @@ def create_account(cli, account, use_ledger=False):
accounts = []
for i, node in enumerate(config["validators"]):
mnemonic = node.get("mnemonic")
account = cli.create_account("validator", i, mnemonic=mnemonic)
coin_type = node.get("coin-type")
account = cli.create_account(
"validator", i, mnemonic=mnemonic, coin_type=coin_type
)
if mnemonic:
account["mnemonic"] = mnemonic
accounts.append(account)
if "coins" in node:
cli.add_genesis_account(account["address"], node["coins"], i)
if "staked" in node:
optional_fields = [
"account_number",
"commission_max_change_rate",
"commission_max_rate",
"commission_rate",
"details",
"security_contact",
"sequence",
"identity",
"website",
"gas_prices",
"gas",
"fees"
]
extra_kwargs = {
name: str(node[name]) for name in optional_fields if name in node
}
gentx_extra_args = [config.get("cmd-flags")]
# build extra positional args; inject --offline if sequence or account_number set
has_acct = "account_number" in extra_kwargs
has_seq = "sequence" in extra_kwargs
if has_acct ^ has_seq: # xor: only one provided
raise ValueError("Both 'account_number' and 'sequence' must be provided together for offline gentx")
if has_acct and has_seq:
gentx_extra_args.append("--offline")
Comment on lines +1052 to +1059
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Avoid passing None or opaque “cmd-flags” into gentx args

gentx_extra_args currently includes config.get("cmd-flags") even when None, which risks propagating None into the underlying CLI builder. Also, if cmd-flags is a list/tuple, flattening is safer.

Apply this diff:

-            gentx_extra_args = [config.get("cmd-flags")]
+            gentx_extra_args = []
+            flags = config.get("cmd-flags")
+            if flags:
+                if isinstance(flags, (list, tuple)):
+                    gentx_extra_args.extend(map(str, flags))
+                else:
+                    gentx_extra_args.append(str(flags))
             # build extra positional args; inject --offline if sequence or account_number set
             has_acct = "account_number" in extra_kwargs
             has_seq = "sequence" in extra_kwargs
             if has_acct ^ has_seq:  # xor: only one provided
                 raise ValueError("Both 'account_number' and 'sequence' must be provided together for offline gentx")
             if has_acct and has_seq:
                 gentx_extra_args.append("--offline")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
gentx_extra_args = [config.get("cmd-flags")]
# build extra positional args; inject --offline if sequence or account_number set
has_acct = "account_number" in extra_kwargs
has_seq = "sequence" in extra_kwargs
if has_acct ^ has_seq: # xor: only one provided
raise ValueError("Both 'account_number' and 'sequence' must be provided together for offline gentx")
if has_acct and has_seq:
gentx_extra_args.append("--offline")
gentx_extra_args = []
flags = config.get("cmd-flags")
if flags:
if isinstance(flags, (list, tuple)):
gentx_extra_args.extend(map(str, flags))
else:
gentx_extra_args.append(str(flags))
# build extra positional args; inject --offline if sequence or account_number set
has_acct = "account_number" in extra_kwargs
has_seq = "sequence" in extra_kwargs
if has_acct ^ has_seq: # xor: only one provided
raise ValueError("Both 'account_number' and 'sequence' must be provided together for offline gentx")
if has_acct and has_seq:
gentx_extra_args.append("--offline")
🤖 Prompt for AI Agents
In pystarport/cluster.py around lines 1050 to 1057, gentx_extra_args currently
unconditionally includes config.get("cmd-flags") which may be None or a non-flat
sequence; change the logic to skip None/empty values and to normalize/flatten
cmd-flags: if cmd-flags is a string, split it into tokens; if it's a list/tuple,
extend gentx_extra_args with its items; if it's a single other value, append it;
ensure you only modify gentx_extra_args when the normalized flags are non-empty
so no None or nested sequences are passed to the CLI builder.

cli.gentx(
"validator",
node["staked"],
config.get("cmd-flags"),
*gentx_extra_args,
i=i,
min_self_delegation=node.get("min_self_delegation", 1),
pubkey=node.get("pubkey"),
Expand Down Expand Up @@ -1111,7 +1135,7 @@ def create_account(cli, account, use_ledger=False):
supervisord_ini(
cmd,
config["validators"],
config["chain_id"],
chain_id,
start_flags=start_flags,
),
)
Expand Down Expand Up @@ -1232,16 +1256,27 @@ def init_cluster(
extension = Path(config_path).suffix
if extension == ".jsonnet":
config = expand_jsonnet(config_path, dotenv)
else:
elif extension in [".yml", ".yaml"]:
config = expand_yaml(config_path, dotenv)
elif extension == ".json":
config = json.loads(Path(config_path).read_text())
else:
raise ValueError("unsupported config file format: " + extension)

relayer_config = config.pop("relayer", {})
for chain_id, cfg in config.items():
cfg["path"] = str(config_path)
cfg["chain_id"] = chain_id

chains = list(config.values())
for chain in chains:

# for multiple chains, there can be multiple cmds splited by `,`
if cmd is not None:
cmds = cmd.split(",")
else:
cmds = [None] * len(chains)

Comment on lines +1273 to +1278
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Edge-case: command count mismatch

zip(chains, cmds) silently drops extra chains if fewer cmds are supplied. Warn or pad cmds to len(chains) to avoid half-initialised clusters.

-    if cmd is not None:
-        cmds = cmd.split(",")
-    else:
-        cmds = [None] * len(chains)
+    cmds = (cmd.split(",") if cmd is not None else [])
+    if len(cmds) < len(chains):
+        cmds.extend([None] * (len(chains) - len(cmds)))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# for multiple chains, there can be multiple cmds splited by `,`
if cmd is not None:
cmds = cmd.split(",")
else:
cmds = [None] * len(chains)
# for multiple chains, there can be multiple cmds splited by `,`
cmds = cmd.split(",") if cmd is not None else []
if len(cmds) < len(chains):
cmds.extend([None] * (len(chains) - len(cmds)))
🧰 Tools
🪛 Ruff (0.11.9)

1256-1259: Use ternary operator cmds = cmd.split(",") if cmd is not None else [None] * len(chains) instead of if-else-block

Replace if-else-block with cmds = cmd.split(",") if cmd is not None else [None] * len(chains)

(SIM108)

🤖 Prompt for AI Agents
In pystarport/cluster.py around lines 1255 to 1260, the code splits cmds by
commas but does not handle cases where the number of cmds is less than the
number of chains, causing zip to drop extra chains silently. Modify the code to
check if cmds length is less than chains length and pad cmds with None or empty
strings to match the length of chains, ensuring all chains are initialized
properly without being dropped.

for chain, cmd in zip(chains, cmds):
(data_dir / chain["chain_id"]).mkdir()
init_devnet(
data_dir / chain["chain_id"], chain, base_port, image, cmd, gen_compose_file
Expand Down Expand Up @@ -1283,6 +1318,8 @@ def init_cluster(
{
"global": {
"api-listen-addr": ":5183",
"debug-listen-addr": ":5183",
"enable-debug-server": True,
"timeout": "10s",
"memo": "",
"light-cache-size": 20,
Expand Down Expand Up @@ -1351,11 +1388,13 @@ def supervisord_ini(cmd, validators, chain_id, start_flags=""):
command=f"{cmd} start --home . {start_flags}",
stdout_logfile=f"{directory}.log",
)

oracle_config = node["app-config"].get("oracle", {})
app_cfg = node.get("app-config", {})
if not app_cfg:
continue
oracle_config = app_cfg.get("oracle", {})
if oracle_config.get("enabled"):
oracle_port = ports.oracle_port(node["base_port"])
grpc_address = node["app-config"].get("grpc", {}).get("address", "")
grpc_address = app_cfg.get("grpc", {}).get("address", "")
grpc_port = grpc_address.split(":")[1] if ":" in grpc_address else ports.grpc_port(node["base_port"])
oracle_section = f"program:{chain_id}-node{i}-oracle"
Comment on lines +1391 to 1399
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: grpc_port extraction breaks for addresses with scheme (e.g., tcp://127.0.0.1:9090)

Using split(":")[1] returns the second segment, which is incorrect when a scheme is present. Use rsplit(":", 1) to reliably capture the port; also preserve the existing fallback.

Apply this diff:

-        grpc_address = app_cfg.get("grpc", {}).get("address", "")
-        grpc_port = grpc_address.split(":")[1] if ":" in grpc_address else ports.grpc_port(node["base_port"])
+        grpc_address = app_cfg.get("grpc", {}).get("address", "")
+        grpc_port = ports.grpc_port(node["base_port"])
+        if ":" in grpc_address:
+            grpc_port = grpc_address.rsplit(":", 1)[-1]

Optionally, guard for IPv6 literals with brackets in the future.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app_cfg = node.get("app-config", {})
if not app_cfg:
continue
oracle_config = app_cfg.get("oracle", {})
if oracle_config.get("enabled"):
oracle_port = ports.oracle_port(node["base_port"])
grpc_address = node["app-config"].get("grpc", {}).get("address", "")
grpc_address = app_cfg.get("grpc", {}).get("address", "")
grpc_port = grpc_address.split(":")[1] if ":" in grpc_address else ports.grpc_port(node["base_port"])
oracle_section = f"program:{chain_id}-node{i}-oracle"
app_cfg = node.get("app-config", {})
if not app_cfg:
continue
oracle_config = app_cfg.get("oracle", {})
if oracle_config.get("enabled"):
oracle_port = ports.oracle_port(node["base_port"])
grpc_address = app_cfg.get("grpc", {}).get("address", "")
grpc_port = ports.grpc_port(node["base_port"])
if ":" in grpc_address:
grpc_port = grpc_address.rsplit(":", 1)[-1]
oracle_section = f"program:{chain_id}-node{i}-oracle"

ini[oracle_section] = dict(
Expand Down
13 changes: 0 additions & 13 deletions pystarport/convert.sh

This file was deleted.

Loading