Files
carrotcoin/wallet.py
2025-12-13 23:41:07 +01:00

234 lines
9.6 KiB
Python
Executable File

#! /usr/bin/env python3
import base64, hashlib, socket, sys, time
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, PublicFormat, NoEncryption
def format_address(raw_address):
return base64.b64encode(raw_address).decode()
def format_amount(amount, width, show_plus = True):
color_prefix = ""
color_suffix = ""
sign = "+ " if show_plus else ""
if amount < 0:
color_prefix = "\x1b[31m"
color_suffix = "\x1b[0m"
sign = "- "
amount = -amount
coins = amount / 100
amount_str = f"{sign}{coins:.02f}"
amount_str = f"{amount_str:>{width}}"
return f"{color_prefix}{amount_str} cc{color_suffix}"
def write_transaction(timestamp, message, amount):
formatted_time = time.strftime("%d.%m.%Y %H:%M:%S", time.localtime(timestamp))
print(f"{formatted_time} {message:<44} {format_amount(amount, 11)}")
def show_balance(public_key):
public_key_raw = public_key.public_bytes(Encoding.Raw, PublicFormat.Raw)
try:
with open("blockchain", "rb") as f:
total_amount = 0
block_counter = 0
pending_bets = [[]]
while True:
block = f.read(293)
if len(block) != 293:
break
miner = block[181:213]
timestamp = int.from_bytes(block[245:253], "big")
if block[0] == 1:
# Payment
sender = block[5:37]
receiver = block[37:69]
amount = int.from_bytes(block[69:77], "big")
fee = int.from_bytes(block[77:85], "big")
if sender == public_key_raw:
write_transaction(timestamp, format_address(receiver), - amount)
if fee > 0:
write_transaction(timestamp, 39 * " " + "(fee)", - fee)
total_amount -= (amount + fee)
if receiver == public_key_raw:
write_transaction(timestamp, format_address(sender), amount)
total_amount += amount
elif block[0] == 2:
# Gambling
transaction_id = block[1:5]
player = block[5:37]
amount = int.from_bytes(block[37:45], "big")
fee = int.from_bytes(block[45:53], "big")
if player == public_key_raw:
write_transaction(timestamp, "- gambling -", -amount)
if fee > 0:
write_transaction(timestamp, 39 * " " + "(fee)", - fee)
total_amount -= (amount + fee)
pending_bets[-1].append((transaction_id, amount))
elif block[0] == 3:
# Reveal transaction
reveal_info = f.read(5376)
revealer = block[1:33]
R = reveal_info[0:256]
relevant_bets = pending_bets[0]
pending_bets = pending_bets[1:]
fee = 100
for (transaction_id, gambling_amount) in relevant_bets:
to_hash = transaction_id + public_key_raw + R
if hashlib.sha256(to_hash).digest()[0] >= 0x80:
write_transaction(timestamp, f"Won gambling with {format_amount(gambling_amount, 0, False)}", 2 * gambling_amount)
total_amount += 2 * gambling_amount
else:
write_transaction(timestamp, f"Lost gambling with {format_amount(gambling_amount, 0, False)}", 0)
if revealer == public_key_raw:
write_transaction(timestamp, "revealing reward", 1500)
total_amount += 1500
else:
fee = 0
if miner == public_key_raw:
write_transaction(timestamp, "mining reward", 100 + fee)
total_amount += 100 + fee
if block_counter % 256 == 255:
pending_bets.append([])
block_counter += 1
print(81 * "\u2500")
amount_string = f"\U0001f955 \x1b[1;37m{format_amount(total_amount, 41, False)}\x1b[0m"
print(21 * " " + f"\x1b[1;37mYour balance:\x1b[0m{amount_string}")
except FileNotFoundError:
print("File \"blockchain\" not found.\nThis wallet script requires a running node with at least 1 block in the current directory.", file=sys.stderr)
exit(1)
def parse_amount(amount):
amount = amount.replace(",", ".")
parts = amount.split(".")
if len(parts) == 1:
return int(parts[0]) * 100
elif len(parts) == 2:
if len(parts[1]) == 0 or len(parts[1]) > 2:
raise Exception(f"Invalid amount: {amount}")
coins = int(parts[0]) if parts[0] != "" else 0
cents = int(parts[1]) if parts[1] != "" else 0
if len(parts[1]) == 1:
cents *= 10
return coins * 100 + cents
raise Exception(f"Invalid amount: {amount}")
def parse_amount_checked(amount):
amount = parse_amount(amount)
if amount < 0:
raise Exception("amount must not be negative")
if amount >= 2**64:
raise Exception("amount is too large")
return amount
def find_free_id(public_key_raw):
try:
with open("blockchain", "rb") as f:
used_ids = set()
while True:
block = f.read(293)
if len(block) != 293:
break
transaction_type = block[0]
if transaction_type in (1, 2):
transaction_id = int.from_bytes(block[1:5], "big")
sender = block[5:37]
if sender == public_key_raw:
used_ids.add(transaction_id)
elif transaction_type == 3:
f.read(5376)
for possible_id in range(0, 2**32):
if possible_id not in used_ids:
return possible_id
raise Exception("No transaction id available")
except FileNotFoundError:
return 0
def send_payment(private_key, target, amount, fee):
target_raw = base64.b64decode(target.encode())
if len(target_raw) != 32:
raise Exception(f"Invalid target address: {target}")
amount = parse_amount_checked(amount)
if amount == 0:
raise Exception("Amount must not be zero")
fee = parse_amount_checked(fee)
public_key_raw = private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
transaction_id = find_free_id(public_key_raw)
transaction_prefix = b"\x01" + \
transaction_id.to_bytes(4, "big") + \
public_key_raw + \
target_raw + \
amount.to_bytes(8, "big") + \
fee.to_bytes(8, "big")
signature = private_key.sign(transaction_prefix)
transaction = transaction_prefix + signature
send_transaction(transaction)
def gamble(private_key, amount, fee):
amount = parse_amount_checked(amount)
if amount == 0:
raise Exception("Amount must not be zero")
fee = parse_amount_checked(fee)
public_key_raw = private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
transaction_id = find_free_id(public_key_raw)
transaction_prefix = b"\x02" + \
transaction_id.to_bytes(4, "big") + \
public_key_raw + \
amount.to_bytes(8, "big") + \
fee.to_bytes(8, "big")
signature = private_key.sign(transaction_prefix)
transaction = transaction_prefix + signature + 32 * b"\0"
send_transaction(transaction)
def send_transaction(transaction):
request = b"\0\0\0\0\x09" + transaction
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
try:
s.connect(("::1", 62039))
s.settimeout(1)
for _ in range(10):
s.send(request)
try:
response = s.recv(1024)
if response == b"\0\0\0\0\x0a":
print("Payment has been transmitted to the node")
return
except TimeoutError:
pass
print("node did not respond")
except ConnectionRefusedError:
print("node is not running")
def usage_info():
print("Usage:", file=sys.stderr)
print(" ./wallet.py # see your past transactions and balance", file=sys.stderr)
print(" ./wallet.py pay <target> <amount> <fee> # send carrotcoins to someone else", file=sys.stderr)
print(" ./wallet.py gamble <amount> <fee> # set an arbitrary amount on a 50:50 bet", file=sys.stderr)
def main():
try:
with open("wallet-key", "rb") as f:
private_key_raw = f.read()
private_key = Ed25519PrivateKey.from_private_bytes(private_key_raw)
except FileNotFoundError:
private_key = Ed25519PrivateKey.generate()
private_key_raw = private_key.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())
with open("wallet-key", "wb") as f:
f.write(private_key_raw)
public_key = private_key.public_key()
wallet_addr = format_address(public_key.public_bytes(Encoding.Raw, PublicFormat.Raw))
print(f"Your wallet address: {wallet_addr}\n")
if len(sys.argv) == 1:
show_balance(public_key)
elif len(sys.argv) == 5 and sys.argv[1] == "pay":
send_payment(private_key, *sys.argv[2:5])
elif len(sys.argv) == 4 and sys.argv[1] == "gamble":
gamble(private_key, *sys.argv[2:4])
else:
usage_info()
exit(1)
if __name__ == '__main__':
main()