v0.99.50: per-payment crypto addresses, fix crypto topup modal loading

parent b4d36755
......@@ -54,7 +54,7 @@ from .auth.qwen import QwenOAuth2
from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model
__version__ = "0.99.49"
__version__ = "0.99.50"
__all__ = [
# Config
"config",
......
......@@ -144,65 +144,46 @@ class CryptoWalletManager:
'derivation_index': index
}
async def get_or_create_user_address(self, user_id: int, crypto_type: str) -> str:
"""Get existing address or create new one for user"""
# Check if user already has address
with self.db._get_connection() as conn:
cursor = conn.cursor()
async def create_payment_address(self, user_id: int, crypto_type: str, payment_id: str) -> str:
"""Derive a fresh address for each payment request"""
placeholder = '?' if self.db.db_type == 'sqlite' else '%s'
cursor.execute(f"""
SELECT address FROM user_crypto_addresses
WHERE user_id = {placeholder} AND crypto_type = {placeholder}
""", (user_id, crypto_type))
existing = cursor.fetchone()
if existing:
return existing[0]
# Get next available index
with self.db._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db.db_type == 'sqlite' else '%s'
cursor.execute(f"""
SELECT COALESCE(MAX(derivation_index), -1) as max_idx
SELECT COALESCE(MAX(derivation_index), -1)
FROM user_crypto_addresses
WHERE crypto_type = {placeholder}
""", (crypto_type,))
max_index = cursor.fetchone()
next_index = cursor.fetchone()[0] + 1
next_index = max_index[0] + 1
# Derive new address
address_info = self.derive_address(crypto_type, next_index)
# Store in database
with self.db._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db.db_type == 'sqlite' else '%s'
cursor.execute(f"""
INSERT INTO user_crypto_addresses
(user_id, crypto_type, address, derivation_path, derivation_index)
VALUES ({placeholder}, {placeholder}, {placeholder}, {placeholder}, {placeholder})
""", (
user_id,
crypto_type,
address_info['address'],
address_info['derivation_path'],
address_info['derivation_index']
))
(user_id, crypto_type, address, derivation_path, derivation_index, payment_id)
VALUES ({placeholder}, {placeholder}, {placeholder}, {placeholder}, {placeholder}, {placeholder})
""", (user_id, crypto_type, address_info['address'],
address_info['derivation_path'], address_info['derivation_index'], payment_id))
conn.commit()
# Create wallet entry
with self.db._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db.db_type == 'sqlite' else '%s'
insert_or_ignore = "INSERT OR IGNORE" if self.db.db_type == 'sqlite' else "INSERT"
on_conflict = "" if self.db.db_type == 'sqlite' else " ON CONFLICT DO NOTHING"
cursor.execute(f"""
INSERT INTO user_crypto_wallets
{insert_or_ignore} INTO user_crypto_wallets
(user_id, crypto_type, balance_crypto, balance_fiat)
VALUES ({placeholder}, {placeholder}, 0, 0)
VALUES ({placeholder}, {placeholder}, 0, 0){on_conflict}
""", (user_id, crypto_type))
conn.commit()
logger.info(f"Created {crypto_type} address for user {user_id}: {address_info['address']}")
logger.info(f"Created {crypto_type} payment address for user {user_id} (payment {payment_id}): {address_info['address']}")
return address_info['address']
async def get_or_create_user_address(self, user_id: int, crypto_type: str) -> str:
"""Legacy: creates a new payment address with a generic payment_id."""
import uuid
return await self.create_payment_address(user_id, crypto_type, f"legacy-{uuid.uuid4().hex[:8]}")
......@@ -61,6 +61,7 @@ class PaymentMigrations:
self._create_notification_tables(cursor, auto_increment, timestamp_default, boolean_type, text_type, decimal_type)
self._create_wallet_tables(cursor, auto_increment, timestamp_default, boolean_type, text_type, decimal_type)
self._add_stripe_customer_id_column(cursor)
self._migrate_per_payment_addresses(cursor)
self._insert_default_data(cursor)
conn.commit()
......@@ -469,6 +470,71 @@ class PaymentMigrations:
)
''')
def _migrate_per_payment_addresses(self, cursor):
"""Migrate user_crypto_addresses to support per-payment addresses (drop unique user+crypto constraint, add payment_id)"""
try:
if self.db_type == 'sqlite':
cursor.execute("PRAGMA table_info(user_crypto_addresses)")
columns = [row[1] for row in cursor.fetchall()]
if 'payment_id' not in columns:
cursor.execute("ALTER TABLE user_crypto_addresses ADD COLUMN payment_id VARCHAR(64)")
logger.info("✅ Added payment_id column to user_crypto_addresses")
# SQLite: recreate table without UNIQUE(user_id, crypto_type) if it exists
# Check via index list
cursor.execute("PRAGMA index_list(user_crypto_addresses)")
indexes = cursor.fetchall()
has_user_crypto_unique = any(
idx[2] == 1 and 'user_id' in (idx[1] or '') or 'crypto' in (idx[1] or '')
for idx in indexes
)
if has_user_crypto_unique:
# Recreate without the constraint
cursor.execute("""
CREATE TABLE IF NOT EXISTS user_crypto_addresses_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
crypto_type VARCHAR(20) NOT NULL,
address VARCHAR(255) NOT NULL UNIQUE,
derivation_path VARCHAR(100) NOT NULL,
derivation_index INTEGER NOT NULL,
payment_id VARCHAR(64),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
""")
cursor.execute("""
INSERT INTO user_crypto_addresses_new
(id, user_id, crypto_type, address, derivation_path, derivation_index, created_at)
SELECT id, user_id, crypto_type, address, derivation_path, derivation_index, created_at
FROM user_crypto_addresses
""")
cursor.execute("DROP TABLE user_crypto_addresses")
cursor.execute("ALTER TABLE user_crypto_addresses_new RENAME TO user_crypto_addresses")
logger.info("✅ Recreated user_crypto_addresses without UNIQUE(user_id, crypto_type)")
else: # mysql
cursor.execute("""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'user_crypto_addresses' AND COLUMN_NAME = 'payment_id'
""")
if not cursor.fetchone():
cursor.execute("ALTER TABLE user_crypto_addresses ADD COLUMN payment_id VARCHAR(64)")
logger.info("✅ Added payment_id column to user_crypto_addresses")
# Drop unique constraint on (user_id, crypto_type) if present
cursor.execute("""
SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE TABLE_NAME = 'user_crypto_addresses'
AND CONSTRAINT_TYPE = 'UNIQUE'
AND CONSTRAINT_NAME != 'address'
""")
for row in cursor.fetchall():
try:
cursor.execute(f"ALTER TABLE user_crypto_addresses DROP INDEX `{row[0]}`")
logger.info(f"✅ Dropped unique constraint {row[0]} from user_crypto_addresses")
except Exception:
pass
except Exception as e:
logger.warning(f"Migration per-payment addresses: {e}")
def _add_stripe_customer_id_column(self, cursor):
"""Add Stripe customer ID column to users table"""
try:
......
......@@ -123,6 +123,13 @@ async def initiate_topup(
metadata={"type": "wallet_topup"}
)
return {"order_id": order.id, "payment_method": "paypal"}
elif topup_data.payment_method in ("bitcoin", "ethereum", "usdt", "usdc"):
import uuid
crypto_type_map = {"bitcoin": "btc", "ethereum": "eth", "usdt": "usdt", "usdc": "usdc"}
crypto_type = crypto_type_map[topup_data.payment_method]
payment_id = uuid.uuid4().hex
address = await payment_service.wallet_manager.create_payment_address(user_id, crypto_type, payment_id)
return {"address": address, "payment_method": topup_data.payment_method, "payment_id": payment_id}
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
......
......@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aisbf"
version = "0.99.49"
version = "0.99.50"
description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md"
license = "GPL-3.0-or-later"
......
......@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup(
name="aisbf",
version="0.99.49",
version="0.99.50",
author="AISBF Contributors",
author_email="stefy@nexlab.net",
description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment