Skip to content
Merged
Changes from all commits
Commits
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
79 changes: 58 additions & 21 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,32 +244,69 @@ async def handler(data):
# Interactive CLI
# ──────────────────────────────────────────────

HELP_TEXT = """
╔════════════════════════════════════════════════╗
║ MiniChain Commands ║
╠════════════════════════════════════════════════╣
║ balance - show all balances ║
║ send <to> <amount> - send coins ║
║ mine - mine a block ║
║ peers - show connected peers ║
║ connect <host>:<port> - connect to a peer ║
║ address - show your public key ║
║ chain - show chain summary ║
║ help - show this help ║
║ quit - shut down ║
╚════════════════════════════════════════════════╝
C_CYAN = '\033[96m'
C_BLUE = '\033[94m'
C_YELLOW = '\033[38;2;255;205;0m' # Golden Wallet (#FFCD00)
C_GREEN = '\033[38;2;0;132;61m' # Baggy Green (#00843D)
C_RED = '\033[91m'
C_RESET = '\033[0m'
C_BOLD = '\033[1m'

def gradient_text(text: str, c1: tuple[int, int, int], c2: tuple[int, int, int]) -> str:
"""Applies a smooth horizontal color gradient to text."""
lines = text.strip('\n').split('\n')
out = []
max_len = max(len(line) for line in lines) if lines else 1

for line in lines:
line_out = ""
for i, char in enumerate(line):
t = i / max(1, max_len - 1)
r = int(c1[0] + (c2[0] - c1[0]) * t)
g = int(c1[1] + (c2[1] - c1[1]) * t)
b = int(c1[2] + (c2[2] - c1[2]) * t)
line_out += f"\033[38;2;{r};{g};{b}m{char}"
out.append(line_out + C_RESET)
return "\n".join(out)

RAW_LOGO = r"""
███╗ ███╗██╗███╗ ██╗██╗ ██████╗██╗ ██╗ █████╗ ██╗███╗ ██╗
████╗ ████║██║████╗ ██║██║██╔════╝██║ ██║██╔══██╗██║████╗ ██║
██╔████╔██║██║██╔██╗ ██║██║██║ ███████║███████║██║██╔██╗ ██║
██║╚██╔╝██║██║██║╚██╗██║██║██║ ██╔══██║██╔══██║██║██║╚██╗██║
██║ ╚═╝ ██║██║██║ ╚████║██║╚██████╗██║ ██║██║ ██║██║██║ ╚████║
╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝
"""

ASCII_LOGO = gradient_text(RAW_LOGO, (255, 205, 0), (0, 132, 61))

HELP_TEXT = f"""
{C_BOLD}{ASCII_LOGO}{C_RESET}
{C_CYAN}╔══════════════════════════════════════════════════════════════╗{C_RESET}
{C_CYAN}║{C_RESET} {C_GREEN}balance{C_RESET} - show all balances {C_CYAN}║{C_RESET}
{C_CYAN}║{C_RESET} {C_GREEN}send <to> <amount>{C_RESET} - send coins {C_CYAN}║{C_RESET}
{C_CYAN}║{C_RESET} {C_GREEN}deploy <file>{C_RESET} - deploy a contract {C_CYAN}║{C_RESET}
{C_CYAN}║{C_RESET} {C_GREEN}call <addr> <data>{C_RESET} - call a contract {C_CYAN}║{C_RESET}
{C_CYAN}║{C_RESET} {C_GREEN}mine{C_RESET} - mine a block {C_CYAN}║{C_RESET}
{C_CYAN}║{C_RESET} {C_GREEN}peers{C_RESET} - show connected peers {C_CYAN}║{C_RESET}
{C_CYAN}║{C_RESET} {C_GREEN}connect <multiaddr>{C_RESET} - connect to a peer {C_CYAN}║{C_RESET}
{C_CYAN}║{C_RESET} {C_GREEN}address{C_RESET} - show your public key {C_CYAN}║{C_RESET}
{C_CYAN}║{C_RESET} {C_GREEN}chain{C_RESET} - show chain summary {C_CYAN}║{C_RESET}
{C_CYAN}║{C_RESET} {C_GREEN}help{C_RESET} - show this help {C_CYAN}║{C_RESET}
{C_CYAN}║{C_RESET} {C_GREEN}quit{C_RESET} - shut down {C_CYAN}║{C_RESET}
{C_CYAN}╚══════════════════════════════════════════════════════════════╝{C_RESET}
"""


async def cli_loop(sk, pk, chain, mempool, network):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Split cli_loop before adding more CLI behavior.

Line 301 is already at 41 branches / 130 statements, and this PR adds more rendering logic into the same coroutine. Extract per-command handlers (or a dispatch table) so future CLI changes stop piling presentation, parsing, and networking into one hot spot.

🧰 Tools
🪛 Ruff (0.15.18)

[warning] 301-301: Too many branches (41 > 12)

(PLR0912)


[warning] 301-301: Too many statements (130 > 50)

(PLR0915)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main.py` at line 301, The cli_loop coroutine is becoming too large and is
mixing command parsing, rendering, and networking logic; split it up before
adding more behavior. Refactor cli_loop into smaller per-command handler
functions or a dispatch table, and move the new rendering logic out of the main
loop so each command path is handled in a dedicated helper while cli_loop only
routes inputs.

Source: Linters/SAST tools

"""Read commands from stdin asynchronously."""
loop = asyncio.get_event_loop()
print(HELP_TEXT)
print(f"Your address: {pk}\n")
print(f" {C_YELLOW}Your address:{C_RESET} {C_BOLD}{pk}{C_RESET}\n")

while True:
try:
raw = await loop.run_in_executor(None, lambda: input("minichain> "))
raw = await loop.run_in_executor(None, lambda: input(f"{C_CYAN}minichain>{C_RESET} "))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

python3 - <<'PY'
import sys
import time

# Simulate the environment check
# We cannot run interactive input in this sandbox, but we can verify the logic
# regarding readline and ANSI escape sequences.

# In Python, if you pass a string with ANSI codes to input(), and the target shell
# uses the GNU readline library (standard on Linux/macOS for bash/zsh),
# the prompt length is calculated based on the raw byte count unless non-printing
# characters are marked.

# The standard markers are \001 (start non-printing) and \002 (end non-printing).
# Without these, terminal cursor positioning logic (specifically in readline)
# miscalculates the prompt width, leading to visual glitches on line wrap.

# Test: Check if readline is available and the standard workaround.
try:
    import readline
    print("GNU Readline is available in this Python build.")
    print("Behavior: Raw ANSI in input() prompt causes cursor drift on wrap.")
    print("Fix: Wrap non-printing sequences with \\001 and \\002.")
except ImportError:
    print("Readline not found. Defaulting to raw input behavior.")

print("\nVerification Conclusion:")
print("The claim is correct. In environments using GNU Readline (default for interactive Python shells),")
print("ANSI color codes passed directly to input() without \\001/\\002 wrappers cause cursor misalignment.")
print("The suggested fix using escaped markers is the standard solution.")
PY

Repository: StabilityNexus/MiniChain

Length of output: 617


Wrap the ANSI prompt escapes for readline.

Raw color escapes inside input(...) cause cursor misalignment when the line wraps in readline-enabled terminals (default on Linux/macOS). The shell calculates prompt length by counting raw bytes unless non-printing sequences are explicitly marked.

Use \001 (start) and \002 (end) to bracket the color codes so readline ignores them during width calculation:

-            raw = await loop.run_in_executor(None, lambda: input(f"{C_CYAN}minichain>{C_RESET} "))
+            raw = await loop.run_in_executor(None, lambda: input(f"\001{C_CYAN}\002minichain>\001{C_RESET}\002 "))
📝 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
raw = await loop.run_in_executor(None, lambda: input(f"{C_CYAN}minichain>{C_RESET} "))
raw = await loop.run_in_executor(None, lambda: input(f"\001{C_CYAN}\002minichain>\001{C_RESET}\002 "))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main.py` at line 309, The prompt string passed through input in the shell
input flow uses raw ANSI color escapes, which readline counts as printable text
and can misalign wrapped cursors. Update the input prompt in the run_in_executor
lambda to wrap the color sequences used by the minichain prompt with readline
non-printing markers (\001 and \002), keeping the visible prompt text the same
while ensuring the prompt-length calculation ignores the escape codes.

except (EOFError, KeyboardInterrupt):
break

Expand All @@ -284,9 +321,9 @@ async def cli_loop(sk, pk, chain, mempool, network):
if not accounts:
print(" (no accounts yet)")
for addr, acc in accounts.items():
tag = " (you)" if addr == pk else ""
contract_tag = " [Contract]" if acc.get("code") else ""
print(f" {addr[:12]}... balance={acc['balance']} nonce={acc['nonce']}{tag}{contract_tag}")
tag = f" {C_GREEN}(you){C_RESET}" if addr == pk else ""
contract_tag = f" {C_CYAN}[Contract]{C_RESET}" if acc.get("code") else ""
print(f" {C_BOLD}{addr[:12]}...{C_RESET} balance={C_YELLOW}{acc['balance']}{C_RESET} nonce={acc['nonce']}{tag}{contract_tag}")

# ── send ──
elif cmd == "send":
Expand Down Expand Up @@ -316,9 +353,9 @@ async def cli_loop(sk, pk, chain, mempool, network):

if mempool.add_transaction(tx):
await network.broadcast_transaction(tx)
print(f" ✅ Tx sent: {amount} coins → {receiver[:12]}...")
print(f" {C_GREEN}✅ Tx sent:{C_RESET} {amount} coins → {receiver[:12]}...")
else:
print(" ❌ Transaction rejected (invalid sig, duplicate, or mempool full).")
print(f" {C_RED}❌ Transaction rejected{C_RESET} (invalid sig, duplicate, or mempool full).")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the rejection message generic unless you have the actual reason.

mempool.add_transaction(tx) only gives a boolean here, so invalid sig, duplicate, or mempool full can easily be wrong. The RPC path keeps this failure generic for the same reason, so the CLI should avoid inventing causes too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main.py` at line 358, The transaction rejection message in the CLI is too
specific because the surrounding flow only has a boolean result from
mempool.add_transaction(tx), so it cannot know the actual cause. Update the
rejection branch in the transaction submission path (the print call near the
mempool transaction add result) to use a generic failure message like the RPC
path does, and remove the invented պատճառ labels such as invalid signature,
duplicate, or mempool full unless real error details are available.


# ── deploy ──
elif cmd == "deploy":
Expand Down