This commit is contained in:
2025-08-05 15:15:36 +02:00
parent 4bd960ed05
commit 7fabb4163a
192 changed files with 14901 additions and 0 deletions

View File

View File

@@ -0,0 +1,241 @@
from dataclasses import dataclass, field, asdict
from typing import List, Optional
from stellar_sdk import Keypair, Server, StrKey
import json
import redis
from stellar.model import StellarAsset, StellarAccount
import os
import csv
import toml
from herotools.texttools import description_fix
class HorizonServer:
def __init__(self, instance: str = "default", network: str = "main", tomlfile: str = "", owner: str = ""):
"""
Load a Stellar account's information using the Horizon server.
The Horizon server is an API that allows interaction with the Stellar network. It provides endpoints to submit transactions, check account balances, and perform other operations on the Stellar ledger.
All gets cached in redis
"""
self.redis_client = redis.Redis(host='localhost', port=6379, db=0) # Adjust as needed
self.instance = instance
if network not in ['main', 'testnet']:
raise ValueError("Invalid network value. Must be 'main' or 'testnet'.")
self.network = network
testnet = self.network == 'testnet'
self.server = Server("https://horizon-testnet.stellar.org" if testnet else "https://horizon.stellar.org")
self.tomlfile = os.path.expanduser(tomlfile)
self.owner = owner
if self.tomlfile:
self.toml_load()
def account_exists(self, pubkey: str) -> bool:
"""
Check if an account exists in the Redis cache based on the public key.
"""
redis_key = f"stellar:{self.instance}:accounts:{pubkey}"
return self.redis_client.exists(redis_key) != None
def account_get(self, key: str, reload: bool = False, name: str = "", description: str = "", cat: str = "") -> StellarAccount:
"""
Load a Stellar account's information.
Args:
key (str): The private or public key of the Stellar account.
reset (bool, optional): Whether to force a refresh of the cached data. Defaults to False.
name (str, optional): Name for the account. Defaults to "".
description (str, optional): Description for the account. Defaults to "".
owner (str, optional): Owner of the account. Defaults to "".
cat (str, optional): Category of the account. Defaults to "".
Returns:
StellarAccount: A struct containing the account's information.
"""
if key == "" and name:
for redis_key in self.redis_client.scan_iter(f"stellar:{self.instance}:accounts:*"):
data = self.redis_client.get(redis_key)
if data:
data = json.loads(str(data))
if data.get('name') == name and data.get('priv_key', data.get('public_key')):
key = data.get('priv_key', data.get('public_key'))
break
if key == "":
raise ValueError("No key provided")
# Determine if the key is a public or private key
if StrKey.is_valid_ed25519_public_key(key):
public_key = key
priv_key = ""
elif StrKey.is_valid_ed25519_secret_seed(key):
priv_key = key
keypair = Keypair.from_secret(priv_key)
public_key = keypair.public_key
else:
raise ValueError("Invalid Stellar key provided")
redis_key = f"stellar:{self.instance}:accounts:{public_key}"
data = self.redis_client.get(redis_key)
changed = False
if data:
try:
data = json.loads(str(data))
except Exception as e:
print(data)
raise e
data['assets'] = [StellarAsset(**asset) for asset in data['assets']]
account = StellarAccount(**data)
if description!="" and description!=account.description:
account.description = description
changed = True
if name!="" and name!=account.name:
account.name = name
changed = True
if self.owner!="" and self.owner!=account.owner:
account.owner = self.owner
changed = True
if cat!="" and cat!=account.cat:
account.cat = cat
changed = True
else:
account = StellarAccount(public_key=public_key, description=description, name=name, priv_key=priv_key, owner=self.owner, cat=cat)
changed = True
if reload or account.assets == []:
changed = True
if reload:
account.assets = []
account_data = self.server.accounts().account_id(public_key).call()
account.assets.clear() # Clear existing assets to avoid duplication
for balance in account_data['balances']:
asset_type = balance['asset_type']
if asset_type == 'native':
account.assets.append(StellarAsset(type="XLM", balance=balance['balance']))
else:
if 'asset_code' in balance:
account.assets.append(StellarAsset(
type=balance['asset_code'],
issuer=balance['asset_issuer'],
balance=balance['balance']
))
changed = True
# Cache the result in Redis for 1 hour if there were changes
if changed:
self.account_save(account)
return account
def comment_add(self, pubkey: str, comment: str, ignore_non_exist: bool = False):
"""
Add a comment to a Stellar account based on the public key.
Args:
pubkey (str): The public key of the Stellar account.
comment (str): The comment to add to the account.
"""
comment = description_fix(comment)
if not self.account_exists(pubkey):
if ignore_non_exist:
return
raise ValueError("Account does not exist in the cache")
account = self.account_get(pubkey)
account.comments.append(comment)
self.account_save(account)
def account_save(self, account: StellarAccount):
"""
Save a Stellar account's information to the Redis cache.
Args:
account (StellarAccount): The account to save.
"""
redis_key = f"stellar:{self.instance}:accounts:{account.public_key}"
self.redis_client.setex(redis_key, 600, json.dumps(asdict(account)))
def reload_cache(self):
"""
Walk over all known accounts and reload their information.
"""
for redis_key in self.redis_client.scan_iter(f"stellar:{self.instance}:accounts:*"):
data = self.redis_client.get(redis_key) or ""
if data:
data = json.loads(str(data))
public_key = data.get('public_key')
if public_key:
self.account_get(public_key, reload=True)
#format is PUBKEY,DESCRIPTION in text format
def load_accounts_csv(self, file_path:str):
file_path=os.path.expanduser(file_path)
if not os.path.exists(file_path):
return Exception(f"Error: File '{file_path}' does not exist.")
try:
with open(file_path, 'r', newline='') as file:
reader = csv.reader(file, delimiter=',')
for row in reader:
if row and len(row) >= 2: # Check if row is not empty and has at least 2 elements
pubkey = row[0].strip()
comment = ','.join(row[1:]).strip()
if self.account_exists(pubkey):
self.comment_add(pubkey, comment)
except IOError as e:
return Exception(f"Error reading file: {e}")
except csv.Error as e:
return Exception(f"Error parsing CSV: {e}")
except Exception as e:
return Exception(f"Error: {e}")
def accounts_get(self) -> List[StellarAccount]:
"""
Retrieve a list of all known Stellar accounts from the Redis cache.
Returns:
List[StellarAccount]: A list of StellarAccount objects.
"""
accounts = []
for redis_key in self.redis_client.scan_iter(f"stellar:{self.instance}:accounts:*"):
pubkey = str(redis_key.split(':')[-1])
accounts.append(self.account_get(key=pubkey))
return accounts
def toml_save(self):
"""
Save the list of all known Stellar accounts to a TOML file.
Args:
file_path (str): The path where the list needs to be saved.
"""
if self.tomlfile == "":
raise ValueError("No TOML file path provided")
accounts = self.accounts_get()
accounts_dict = {account.public_key: asdict(account) for account in accounts}
with open(self.tomlfile, 'w') as file:
toml.dump( accounts_dict, file)
def toml_load(self):
"""
Load the list of Stellar accounts from a TOML file and save them to the Redis cache.
Args:
file_path (str): The path of the TOML file to load.
"""
if not os.path.exists(self.tomlfile):
return
#raise FileNotFoundError(f"Error: File '{self.tomlfile}' does not exist.")
with open(self.tomlfile, 'r') as file:
accounts_dict = toml.load(file)
for pubkey, account_data in accounts_dict.items():
account_data['assets'] = [StellarAsset(**asset) for asset in account_data['assets']]
account = StellarAccount(**account_data)
self.account_save(account)
def new(instance: str = "default",owner: str = "", network: str = "main", tomlfile: str = "") -> HorizonServer:
return HorizonServer(instance=instance, network=network, tomlfile=tomlfile,owner=owner)

View File

@@ -0,0 +1,70 @@
from dataclasses import dataclass, field, asdict
from typing import List, Optional
from stellar_sdk import Keypair, Server, StrKey
import json
import redis
@dataclass
class StellarAsset:
type: str
balance: float
issuer: str = ""
def format_balance(self):
balance_float = float(self.balance)
formatted_balance = f"{balance_float:,.2f}"
if '.' in formatted_balance:
formatted_balance = formatted_balance.rstrip('0').rstrip('.')
return formatted_balance
def md(self):
formatted_balance = self.format_balance()
return f"- **{self.type}**: {formatted_balance}"
@dataclass
class StellarAccount:
owner: str
priv_key: str = ""
public_key: str = ""
assets: List[StellarAsset] = field(default_factory=list)
name: str = ""
description: str = ""
comments: List[str] = field(default_factory=list)
cat: str = ""
question: str = ""
def md(self):
result = [
f"# Stellar Account: {self.name or 'Unnamed'}","",
f"**Public Key**: {self.public_key}",
f"**Cat**: {self.cat}",
f"**Description**: {self.description[:60]}..." if self.description else "**Description**: None",
f"**Question**: {self.question}" if self.question else "**Question**: None",
"",
"## Assets:",""
]
for asset in self.assets:
result.append(asset.md())
if len(self.assets) == 0:
result.append("- No assets")
result.append("")
if self.comments:
result.append("## Comments:")
for comment in self.comments:
if '\n' in comment:
multiline_comment = "\n ".join(comment.split('\n'))
result.append(f"- {multiline_comment}")
else:
result.append(f"- {comment}")
return "\n".join(result)
def balance_str(self) -> str:
out=[]
for asset in self.assets:
out.append(f"{asset.type}:{float(asset.balance):,.0f}")
return " ".join(out)

View File

@@ -0,0 +1,78 @@
module stellar
import freeflowuniverse.crystallib.core.texttools
pub struct DigitalAssets {
pub mut:
}
pub struct Owner {
pub mut:
name string
accounts []Account
}
@[params]
pub struct AccountGetArgs{
pub mut:
name string
bctype BlockChainType
}
pub fn (self DigitalAssets) account_get(args_ AccountGetArgs) !&Account {
mut accounts := []&Account
mut args:=args_
args.name = texttools.name_fix(args.name)
for account in self.accounts {
if account.name == args.name && account.bctype == args.bctype {
accounts<<&account
}
}
if accounts.len == 0 {
return error('No account found with the given name:${args.name} and blockchain type: ${args.bctype}')
} else if count > 1 {
return error('Multiple accounts found with the given name:${args.name} and blockchain type: ${args.bctype}')
}
return accounts[0]
}
pub struct Account {
pub mut:
name string
secret string
pubkey string
description string
cat string
owner string
assets []Asset
bctype BlockChainType
}
pub struct Asset {
pub mut:
amount int
assettype AssetType
}
pub fn (self Asset) name() string {
return self.assettype.name
}
pub struct AssetType {
pub mut:
name string
issuer string
bctype BlockChainType
}
pub enum BlockChainType{
stellar_pub
stellar_test
}

View File

@@ -0,0 +1,46 @@
from typing import Tuple
from stellar_sdk import Server, Keypair, TransactionBuilder, Network, Asset, Signer, TransactionEnvelope
import redis
import requests
import json
import time
def create_account_on_testnet() -> Tuple[str, str]:
def fund(public_key: str) -> float:
# Request funds from the Stellar testnet friendbot
response = requests.get(f"https://friendbot.stellar.org?addr={public_key}")
if response.status_code != 200:
raise Exception("Failed to fund new account with friendbot")
time.sleep(1)
return balance(public_key)
def create_account() -> Tuple[str, str]:
# Initialize Redis client
redis_client = redis.StrictRedis(host='localhost', port=6379, db=0)
# Generate keypair
keypair = Keypair.random()
public_key = keypair.public_key
secret_key = keypair.secret
account_data = {
"public_key": public_key,
"secret_key": secret_key
}
redis_client.set("stellartest:testaccount", json.dumps(account_data))
time.sleep(1)
return public_key, secret_key
# Check if the account already exists in Redis
if redis_client.exists("stellartest:testaccount"):
account_data = json.loads(redis_client.get("stellartest:testaccount"))
public_key = account_data["public_key"]
secret_key = account_data["secret_key"]
r = balance(public_key)
if r < 100:
fund(public_key)
r = balance(public_key)
return public_key, secret_key
else:
create_account()
return create_account_on_testnet()