...
This commit is contained in:
0
lib/clients/stellar/__init__.py
Normal file
0
lib/clients/stellar/__init__.py
Normal file
241
lib/clients/stellar/horizon.py
Normal file
241
lib/clients/stellar/horizon.py
Normal 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)
|
70
lib/clients/stellar/model.py
Normal file
70
lib/clients/stellar/model.py
Normal 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)
|
78
lib/clients/stellar/model_accounts.v
Normal file
78
lib/clients/stellar/model_accounts.v
Normal 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
|
||||
|
||||
}
|
46
lib/clients/stellar/testnet.py
Normal file
46
lib/clients/stellar/testnet.py
Normal 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()
|
Reference in New Issue
Block a user