diff --git a/.windsurf/workflows/multi-node-blockchain-setup.md b/.windsurf/workflows/multi-node-blockchain-setup.md index c18dec62..58e5c8e6 100644 --- a/.windsurf/workflows/multi-node-blockchain-setup.md +++ b/.windsurf/workflows/multi-node-blockchain-setup.md @@ -259,25 +259,15 @@ fi ### 5. Create Wallet on aitbc ```bash -# On aitbc, create a new wallet using AITBC CLI tool -# Note: CLI tool may not be fully implemented - fallback to Python script if needed -if ssh aitbc "source /opt/aitbc/venv/bin/activate && aitbc wallet create --name aitbc-user --password-file /var/lib/aitbc/keystore/.password" 2>/dev/null; then - echo "Wallet created using CLI tool" -else - echo "CLI not fully implemented, using Python script fallback..." - ssh aitbc 'cd /opt/aitbc/apps/blockchain-node && /opt/aitbc/venv/bin/python scripts/keystore.py --name aitbc-user --create --password $(cat /var/lib/aitbc/keystore/.password)' -fi +# On aitbc, create a new wallet using AITBC simple wallet CLI +ssh aitbc 'python /opt/aitbc/cli/simple_wallet.py create --name aitbc-user --password-file /var/lib/aitbc/keystore/.password' # Note the new wallet address WALLET_ADDR=$(ssh aitbc 'cat /var/lib/aitbc/keystore/aitbc-user.json | jq -r .address') echo "New wallet: $WALLET_ADDR" # Verify wallet was created successfully -if ssh aitbc "source /opt/aitbc/venv/bin/activate && aitbc wallet list --format json" 2>/dev/null; then - ssh aitbc "source /opt/aitbc/venv/bin/activate && aitbc wallet list --format json | jq '.[] | select(.name == \"aitbc-user\")'" -else - echo "Wallet created (CLI list not available)" -fi +ssh aitbc "python /opt/aitbc/cli/simple_wallet.py list --format json | jq '.[] | select(.name == \"aitbc-user\")'" ``` **🔑 Wallet Attachment & Coin Access:** @@ -297,57 +287,17 @@ The newly created wallet on aitbc will: ### 6. Send 1000 AIT from Genesis to aitbc Wallet ```bash -# On aitbc1, send 1000 AIT using AITBC CLI tool -# Note: CLI tool may not be fully implemented - fallback to manual method if needed -source /opt/aitbc/venv/bin/activate - -if aitbc wallet send --from aitbc1genesis --to $WALLET_ADDR --amount 1000 --password-file /var/lib/aitbc/keystore/.password --fee 10 2>/dev/null; then - echo "Transaction sent using CLI tool" - # Get transaction hash for verification - TX_HASH=$(aitbc wallet transactions --from aitbc1genesis --limit 1 --format json 2>/dev/null | jq -r '.[0].hash' || echo "Transaction hash retrieval failed") -else - echo "CLI not fully implemented, using manual transaction method..." - # Manual transaction method (fallback) - GENESIS_KEY=$(/opt/aitbc/venv/bin/python -c " -import json, sys -from cryptography.hazmat.primitives.asymmetric import ed25519 -from cryptography.hazmat.primitives.ciphers.aead import AESGCM -from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC -from cryptography.hazmat.primitives import hashes - -with open('/var/lib/aitbc/keystore/aitbc1genesis.json') as f: - ks = json.load(f) - -# Decrypt private key -crypto = ks['crypto'] -salt = bytes.fromhex(crypto['kdfparams']['salt']) -kdf = PBKDF2HMAC(hashes.SHA256(), 32, salt, crypto['kdfparams']['c']) -key = kdf.derive('aitbc123'.encode()) -aesgcm = AESGCM(key) -nonce = bytes.fromhex(crypto['cipherparams']['nonce']) -priv = aesgcm.decrypt(nonce, bytes.fromhex(crypto['ciphertext']), None) -print(priv.hex()) -") - - # Create and submit transaction - TX_JSON=$(cat << EOF -{ - "sender": "$(cat /var/lib/aitbc/keystore/aitbc1genesis.json | jq -r .address)", - "recipient": "$WALLET_ADDR", - "value": 1000, - "fee": 10, - "nonce": 0, - "type": "transfer", - "payload": {} -} -EOF -) - - curl -X POST http://localhost:8006/sendTx \ - -H "Content-Type: application/json" \ - -d "$TX_JSON" -fi +# On aitbc1, send 1000 AIT using AITBC simple wallet CLI +python /opt/aitbc/cli/simple_wallet.py send \ + --from aitbc1genesis \ + --to $WALLET_ADDR \ + --amount 1000 \ + --fee 10 \ + --password-file /var/lib/aitbc/keystore/.password \ + --rpc-url http://localhost:8006 +# Get transaction hash for verification (simplified - using RPC to check latest transaction) +TX_HASH=$(curl -s http://localhost:8006/rpc/transactions --limit 1 | jq -r '.transactions[0].hash' 2>/dev/null || echo "Transaction hash retrieval failed") echo "Transaction hash: $TX_HASH" # Wait for transaction to be mined diff --git a/cli/simple_wallet.py b/cli/simple_wallet.py new file mode 100644 index 00000000..57aed025 --- /dev/null +++ b/cli/simple_wallet.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +""" +Simple wallet operations for AITBC blockchain +Compatible with existing keystore structure +""" + +import json +import sys +import os +import argparse +from pathlib import Path +from cryptography.hazmat.primitives.asymmetric import ed25519 +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.primitives import hashes +import requests +from typing import Optional + +# Default paths +DEFAULT_KEYSTORE_DIR = Path("/var/lib/aitbc/keystore") +DEFAULT_RPC_URL = "http://localhost:8006" + + +def decrypt_private_key(keystore_path: Path, password: str) -> str: + """Decrypt private key from keystore file""" + with open(keystore_path) as f: + ks = json.load(f) + + crypto = ks['crypto'] + salt = bytes.fromhex(crypto['kdfparams']['salt']) + kdf = PBKDF2HMAC(hashes.SHA256(), 32, salt, crypto['kdfparams']['c']) + key = kdf.derive(password.encode()) + aesgcm = AESGCM(key) + nonce = bytes.fromhex(crypto['cipherparams']['nonce']) + priv = aesgcm.decrypt(nonce, bytes.fromhex(crypto['ciphertext']), None) + return priv.hex() + + +def create_wallet(name: str, password: str, keystore_dir: Path = DEFAULT_KEYSTORE_DIR) -> str: + """Create a new wallet""" + keystore_dir.mkdir(parents=True, exist_ok=True) + + # Generate new key pair + private_key = ed25519.Ed25519PrivateKey.generate() + private_key_hex = private_key.private_bytes_raw().hex() + public_key = private_key.public_key() + public_key_hex = public_key.public_bytes_raw().hex() + + # Calculate address (simplified - in real implementation this would be more complex) + address = f"ait1{public_key_hex[:40]}" + + # Encrypt private key + salt = os.urandom(32) + kdf = PBKDF2HMAC(hashes.SHA256(), 32, salt, 100000) + key = kdf.derive(password.encode()) + aesgcm = AESGCM(key) + nonce = os.urandom(12) + ciphertext = aesgcm.encrypt(nonce, bytes.fromhex(private_key_hex), None) + + # Create keystore file + keystore_data = { + "address": address, + "public_key": public_key_hex, + "crypto": { + "kdf": "pbkdf2", + "kdfparams": { + "salt": salt.hex(), + "c": 100000, + "dklen": 32, + "prf": "hmac-sha256" + }, + "cipher": "aes-256-gcm", + "cipherparams": { + "nonce": nonce.hex() + }, + "ciphertext": ciphertext.hex() + }, + "version": 1 + } + + keystore_path = keystore_dir / f"{name}.json" + with open(keystore_path, 'w') as f: + json.dump(keystore_data, f, indent=2) + + print(f"Wallet created: {name}") + print(f"Address: {address}") + print(f"Keystore: {keystore_path}") + + return address + + +def send_transaction(from_wallet: str, to_address: str, amount: float, fee: float, + password: str, keystore_dir: Path = DEFAULT_KEYSTORE_DIR, + rpc_url: str = DEFAULT_RPC_URL) -> Optional[str]: + """Send transaction from one wallet to another""" + + # Get sender wallet info + sender_keystore = keystore_dir / f"{from_wallet}.json" + if not sender_keystore.exists(): + print(f"Error: Wallet '{from_wallet}' not found") + return None + + with open(sender_keystore) as f: + sender_data = json.load(f) + + sender_address = sender_data['address'] + + # Decrypt private key + try: + private_key_hex = decrypt_private_key(sender_keystore, password) + private_key = ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex(private_key_hex)) + except Exception as e: + print(f"Error decrypting wallet: {e}") + return None + + # Create transaction + transaction = { + "sender": sender_address, + "recipient": to_address, + "value": int(amount), + "fee": int(fee), + "nonce": 0, # In real implementation, get current nonce + "type": "transfer", + "payload": {} + } + + # Sign transaction (simplified) + message = json.dumps(transaction, sort_keys=True).encode() + signature = private_key.sign(message) + transaction["signature"] = signature.hex() + + # Submit transaction + try: + response = requests.post(f"{rpc_url}/sendTx", json=transaction) + if response.status_code == 200: + result = response.json() + print(f"Transaction submitted successfully") + print(f"From: {sender_address}") + print(f"To: {to_address}") + print(f"Amount: {amount} AIT") + print(f"Fee: {fee} AIT") + return result.get("hash") + else: + print(f"Error submitting transaction: {response.text}") + return None + except Exception as e: + print(f"Error: {e}") + return None + + +def list_wallets(keystore_dir: Path = DEFAULT_KEYSTORE_DIR) -> list: + """List all wallets""" + wallets = [] + if keystore_dir.exists(): + for wallet_file in keystore_dir.glob("*.json"): + try: + with open(wallet_file) as f: + data = json.load(f) + wallets.append({ + "name": wallet_file.stem, + "address": data["address"], + "file": str(wallet_file) + }) + except Exception: + pass + return wallets + + +def main(): + parser = argparse.ArgumentParser(description="AITBC Wallet CLI") + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # Create wallet command + create_parser = subparsers.add_parser("create", help="Create a new wallet") + create_parser.add_argument("--name", required=True, help="Wallet name") + create_parser.add_argument("--password", help="Wallet password") + create_parser.add_argument("--password-file", help="File containing wallet password") + + # Send transaction command + send_parser = subparsers.add_parser("send", help="Send AIT") + send_parser.add_argument("--from", required=True, dest="from_wallet", help="From wallet name") + send_parser.add_argument("--to", required=True, dest="to_address", help="To address") + send_parser.add_argument("--amount", type=float, required=True, help="Amount to send") + send_parser.add_argument("--fee", type=float, default=10.0, help="Transaction fee") + send_parser.add_argument("--password", help="Wallet password") + send_parser.add_argument("--password-file", help="File containing wallet password") + send_parser.add_argument("--rpc-url", default=DEFAULT_RPC_URL, help="RPC URL") + + # List wallets command + list_parser = subparsers.add_parser("list", help="List wallets") + list_parser.add_argument("--format", choices=["table", "json"], default="table", help="Output format") + + args = parser.parse_args() + + if args.command == "create": + # Get password + password = None + if args.password: + password = args.password + elif args.password_file: + with open(args.password_file) as f: + password = f.read().strip() + else: + import getpass + password = getpass.getpass("Enter wallet password: ") + + if not password: + print("Error: Password is required") + sys.exit(1) + + address = create_wallet(args.name, password) + print(f"Wallet address: {address}") + + elif args.command == "send": + # Get password + password = None + if args.password: + password = args.password + elif args.password_file: + with open(args.password_file) as f: + password = f.read().strip() + else: + import getpass + password = getpass.getpass(f"Enter password for wallet '{args.from_wallet}': ") + + if not password: + print("Error: Password is required") + sys.exit(1) + + tx_hash = send_transaction( + args.from_wallet, + args.to_address, + args.amount, + args.fee, + password, + rpc_url=args.rpc_url + ) + + if tx_hash: + print(f"Transaction hash: {tx_hash}") + else: + sys.exit(1) + + elif args.command == "list": + wallets = list_wallets() + + if args.format == "json": + print(json.dumps(wallets, indent=2)) + else: + print("Wallets:") + for wallet in wallets: + print(f" {wallet['name']}: {wallet['address']}") + + else: + parser.print_help() + + +if __name__ == "__main__": + main()