Skip to content
Open
Show file tree
Hide file tree
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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ A CLI tool to interact with Monad's staking contract and execute operations by i
- Compounding rewards
- Changing Validator commission
- Querying staking state on the chain
- Transferring funds between addresses

## Security Notes

Expand Down Expand Up @@ -125,19 +126,20 @@ $ source cli-venv/bin/activate
$ python staking-cli/main.py --help

usage: main.py [-h]
{add-validator,delegate,undelegate,withdraw,claim-rewards,compound-rewards,change-commission,query,tui} ...
{add-validator,delegate,undelegate,withdraw,claim-rewards,compound-rewards,change-commission,transfer,query,tui} ...

Staking CLI for Validators on Monad

positional arguments:
{add-validator,delegate,undelegate,withdraw,claim-rewards,compound-rewards,change-commission,query,tui}
{add-validator,delegate,undelegate,withdraw,claim-rewards,compound-rewards,change-commission,transfer,query,tui}
add-validator Add a new validator to network
delegate Delegate to a validator in the network
undelegate Undelegate Stake from validator
withdraw Withdraw undelegated stake from validator
claim-rewards Claim staking rewards
compound-rewards Compound rewards to validator
change-commission Change validator commission
transfer Transfer MON to an address
query Query network information
tui Use a menu-driven TUI

Expand Down Expand Up @@ -170,8 +172,9 @@ $ python staking-cli/main.py tui
│ │
│ 8. Query │
│ │
│ 9. Exit
│ 9. Transfer
│ │
│ 10. Exit │
│ │
╰──────────────────────────────────────╯
```
Expand Down
12 changes: 12 additions & 0 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@ INFO Commission successfully changed from 10.0% to 5.0% for validator 1

**Note:** Only the Validator's authorized address can change the commission.

### Transfer MON

Transfer MON from the validator's account to a specified address.

```sh
python main.py transfer \
--address 0x123... \
--amount 1000 \
--config-path ~/config.toml \
--force # optional: skip confirmation
```

## Query Commands

### Query Validator Information
Expand Down
11 changes: 9 additions & 2 deletions staking-cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from src.parser import init_parser
from src.helpers import number_prompt, confirmation_prompt
from src.signer import create_signer
from src.transfer import transfer, transfer_cli

class StakingCLI:
def __init__(self):
Expand Down Expand Up @@ -91,7 +92,8 @@ def tui(self):
[{self.colors["primary_text"]}]6. Compound[/]\n
[{self.colors["primary_text"]}]7. Change Commission[/]\n
[{self.colors["primary_text"]}]8. Query[/]\n
[{self.colors["primary_text"]}]9. Exit[/]\n
[{self.colors["primary_text"]}]9. Transfer[/]\n
[{self.colors["primary_text"]}]10. Exit[/]\n
'''
menu_text = Align(menu_text, align="left")
main_panel = Panel(
Expand All @@ -101,7 +103,7 @@ def tui(self):
expand=False
)
while True:
choices = [str(x) for x in range(1,10)]
choices = [str(x) for x in range(1,11)]
self.console.print(main_panel)
choice = number_prompt("Enter a number as a choice", choices, default="9")

Expand Down Expand Up @@ -130,6 +132,9 @@ def tui(self):
query(self.config, self.signer)
self.log.info("Exited Query Menu\n\n")
elif choice == "9":
transfer(self.config, self.signer)
self.log.info("Exited Transfer\n\n")
elif choice == "10":
self.log.info("Staking CLI has been exited!")
sys.exit()

Expand Down Expand Up @@ -172,6 +177,8 @@ def main(self):
change_validator_commission_cli(self.config, self.signer, validator_id, commission_percentage)
elif self.args.command == "query":
query_cli(self.config, self.args)
elif self.args.command == "transfer":
transfer_cli(self.config, self.signer, self.args)


if __name__ == "__main__":
Expand Down
28 changes: 28 additions & 0 deletions staking-cli/src/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ def init_parser() -> argparse.ArgumentParser:
change_commission_parser = subparsers.add_parser(
"change-commission", help="Change validator commission"
)
transfer_parser = subparsers.add_parser(
"transfer", help="Transfer MON to an address"
)
query_parser = subparsers.add_parser("query", help="Query network information")
tui_parser = subparsers.add_parser("tui", help="Use a menu-driven TUI")

Expand Down Expand Up @@ -185,6 +188,31 @@ def init_parser() -> argparse.ArgumentParser:
help="Add a path to a config.toml file",
)

# transfer_parser
transfer_parser.add_argument(
"--address",
type=str,
required=True,
help="Recipient address",
)
transfer_parser.add_argument(
"--amount",
type=int,
required=True,
help="Amount in MON to transfer",
)
transfer_parser.add_argument(
"--force",
action="store_true",
help="Skip confirmation",
)
transfer_parser.add_argument(
"--config-path",
type=str,
default="./config.toml",
help="Add a path to a config.toml file",
)

# query_parser
query_subparser = query_parser.add_subparsers(dest="query")
val_info_parser = query_subparser.add_parser(
Expand Down
140 changes: 140 additions & 0 deletions staking-cli/src/transfer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@

from rich.console import Console
from rich.table import Table
from src.helpers import (
address_prompt,
amount_prompt,
confirmation_prompt,
send_transaction,
wei,
)
from src.logger import init_logging
from web3 import Web3

from staking_sdk_py.signer_factory import Signer

console = Console()


def transfer(config: dict, signer: Signer):
# read config
rpc_url = config["rpc_url"]
chain_id = config["chain_id"]

w3 = Web3(Web3.HTTPProvider(rpc_url))
from_address = signer.get_address()

# Parameters for transfer
to_address = address_prompt(config, "recipient")
transfer_amount = amount_prompt(config, description="to transfer")
amount = wei(transfer_amount)

# Check balance
balance = w3.eth.get_balance(from_address)
if balance < amount:
console.print("[bold red]Insufficient balance![/]")
return

table = Table(
show_header=False,
title="Transfer Script Inputs",
title_style="red bold",
expand=True,
show_lines=True,
)
table.add_column("Inputs")
table.add_column("Values")
table.add_row("[cyan][bold red]Recipient Address[/]:[/]", f"[green]{to_address}[/]")
table.add_row(
"[cyan][bold red]Amount to Transfer[/]:[/]",
f"[green]{transfer_amount} MON ({amount} wei)[/]",
)
table.add_row("[cyan][bold red]From Address[/]:[/]", f"[green]{from_address}[/]")
table.add_row("[cyan][bold red]RPC[/]:[/]", f"[green]{rpc_url}[/]")
console.print(table)

is_confirmed = confirmation_prompt(
"Do the inputs above look correct?", default=False
)

if is_confirmed:
# Generate transaction
try:
tx_hash = send_transaction(w3, signer, to_address, "0x", chain_id, amount)
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
except Exception as e:
console.print(f"Error! while trying to send tx: {e}")
return

# Create formatted transaction summary
tx_table = Table(title="Transaction Results", show_header=False, expand=True)
tx_table.add_column("Field", style="cyan")
tx_table.add_column("Value", style="green")
tx_table.add_row("Status", "✅ Success" if receipt.status == 1 else "❌ Failed")
tx_table.add_row("Transaction Hash", "0x" + receipt.transactionHash.hex())
tx_table.add_row("Block Number", str(receipt.blockNumber))
tx_table.add_row("Gas Used", f"{receipt.gasUsed:,}")
tx_table.add_row("From", receipt["from"])
tx_table.add_row("To", receipt.to)
console.print(tx_table)


def transfer_cli(config: dict, signer: Signer, args):
log = init_logging(config["log_level"])
# read config
rpc_url = config["rpc_url"]
chain_id = config["chain_id"]

w3 = Web3(Web3.HTTPProvider(rpc_url))
from_address = signer.get_address()

to_address = args.address
amount_mon = args.amount
force = args.force

# Validate address
if not Web3.is_address(to_address):
log.error("Invalid recipient address")
return
to_address = Web3.to_checksum_address(to_address)

# Get balance
balance_wei = w3.eth.get_balance(from_address)

amount_wei = wei(amount_mon)

# Check sufficient balance
if balance_wei < amount_wei:
log.error(
f"Insufficient balance. Have {balance_wei / 10**18:.2f} MON, need {amount_mon:.2f} MON"
)
return

# Confirmation
if not force:
console = Console()
table = Table(
show_header=False,
title="Transfer Details",
expand=True,
)
table.add_column("Field")
table.add_column("Value")
table.add_row("Recipient Address", to_address)
table.add_row("Amount", f"{amount_mon:.2f} MON ({amount_wei} wei)")
table.add_row("From Address", from_address)
console.print(table)
is_confirmed = confirmation_prompt("Confirm transfer?", default=False)
if not is_confirmed:
log.info("Transfer cancelled")
return

# Send transaction
try:
tx_hash = send_transaction(w3, signer, to_address, "0x", chain_id, amount_wei)
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
except Exception as e:
log.error(f"Error while sending tx: {e}")
return

log.info(f"Tx hash: 0x{receipt.transactionHash.hex()}")