feat: complete remaining phase 1 tasks - multi-chain wallet, atomic swaps, and multi-region deployment

This commit is contained in:
oib
2026-02-28 23:11:55 +01:00
parent 0e6c9eda72
commit bf95cd0d9b
26 changed files with 8091 additions and 6 deletions

View File

@@ -0,0 +1,194 @@
import pytest
from datetime import datetime, timedelta
import secrets
import hashlib
from unittest.mock import AsyncMock
from sqlmodel import Session, create_engine, SQLModel
from sqlmodel.pool import StaticPool
from fastapi import HTTPException
from app.services.atomic_swap_service import AtomicSwapService
from app.domain.atomic_swap import SwapStatus, AtomicSwapOrder
from app.schemas.atomic_swap import SwapCreateRequest, SwapActionRequest, SwapCompleteRequest
@pytest.fixture
def test_db():
engine = create_engine(
"sqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
SQLModel.metadata.create_all(engine)
session = Session(engine)
yield session
session.close()
@pytest.fixture
def mock_contract_service():
return AsyncMock()
@pytest.fixture
def swap_service(test_db, mock_contract_service):
return AtomicSwapService(session=test_db, contract_service=mock_contract_service)
@pytest.mark.asyncio
async def test_create_swap_order(swap_service):
request = SwapCreateRequest(
initiator_agent_id="agent-A",
initiator_address="0xA",
source_chain_id=1,
source_token="0xTokenA",
source_amount=100.0,
participant_agent_id="agent-B",
participant_address="0xB",
target_chain_id=137,
target_token="0xTokenB",
target_amount=200.0,
source_timelock_hours=48,
target_timelock_hours=24
)
order = await swap_service.create_swap_order(request)
assert order.initiator_agent_id == "agent-A"
assert order.status == SwapStatus.CREATED
assert order.hashlock.startswith("0x")
assert order.secret is not None
assert order.source_timelock > order.target_timelock
@pytest.mark.asyncio
async def test_create_swap_invalid_timelocks(swap_service):
request = SwapCreateRequest(
initiator_agent_id="agent-A",
initiator_address="0xA",
source_chain_id=1,
source_token="0xTokenA",
source_amount=100.0,
participant_agent_id="agent-B",
participant_address="0xB",
target_chain_id=137,
target_token="0xTokenB",
target_amount=200.0,
source_timelock_hours=24, # Invalid: not strictly greater than target
target_timelock_hours=24
)
with pytest.raises(HTTPException) as exc_info:
await swap_service.create_swap_order(request)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_swap_lifecycle_success(swap_service):
# 1. Create
request = SwapCreateRequest(
initiator_agent_id="agent-A",
initiator_address="0xA",
source_chain_id=1,
source_token="0xTokenA",
source_amount=100.0,
participant_agent_id="agent-B",
participant_address="0xB",
target_chain_id=137,
target_token="0xTokenB",
target_amount=200.0
)
order = await swap_service.create_swap_order(request)
swap_id = order.id
secret = order.secret
# 2. Initiate
action_req = SwapActionRequest(tx_hash="0xTxInitiate")
order = await swap_service.mark_initiated(swap_id, action_req)
assert order.status == SwapStatus.INITIATED
# 3. Participate
action_req = SwapActionRequest(tx_hash="0xTxParticipate")
order = await swap_service.mark_participating(swap_id, action_req)
assert order.status == SwapStatus.PARTICIPATING
# 4. Complete
comp_req = SwapCompleteRequest(tx_hash="0xTxComplete", secret=secret)
order = await swap_service.complete_swap(swap_id, comp_req)
assert order.status == SwapStatus.COMPLETED
@pytest.mark.asyncio
async def test_complete_swap_invalid_secret(swap_service):
request = SwapCreateRequest(
initiator_agent_id="agent-A",
initiator_address="0xA",
source_chain_id=1,
source_token="native",
source_amount=1.0,
participant_agent_id="agent-B",
participant_address="0xB",
target_chain_id=137,
target_token="native",
target_amount=2.0
)
order = await swap_service.create_swap_order(request)
swap_id = order.id
await swap_service.mark_initiated(swap_id, SwapActionRequest(tx_hash="0x1"))
await swap_service.mark_participating(swap_id, SwapActionRequest(tx_hash="0x2"))
comp_req = SwapCompleteRequest(tx_hash="0x3", secret="wrong_secret")
with pytest.raises(HTTPException) as exc_info:
await swap_service.complete_swap(swap_id, comp_req)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_refund_swap_too_early(swap_service, test_db):
request = SwapCreateRequest(
initiator_agent_id="agent-A",
initiator_address="0xA",
source_chain_id=1,
source_token="native",
source_amount=1.0,
participant_agent_id="agent-B",
participant_address="0xB",
target_chain_id=137,
target_token="native",
target_amount=2.0
)
order = await swap_service.create_swap_order(request)
swap_id = order.id
await swap_service.mark_initiated(swap_id, SwapActionRequest(tx_hash="0x1"))
# Timelock has not expired yet
action_req = SwapActionRequest(tx_hash="0xRefund")
with pytest.raises(HTTPException) as exc_info:
await swap_service.refund_swap(swap_id, action_req)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_refund_swap_success(swap_service, test_db):
request = SwapCreateRequest(
initiator_agent_id="agent-A",
initiator_address="0xA",
source_chain_id=1,
source_token="native",
source_amount=1.0,
participant_agent_id="agent-B",
participant_address="0xB",
target_chain_id=137,
target_token="native",
target_amount=2.0,
source_timelock_hours=48,
target_timelock_hours=24
)
order = await swap_service.create_swap_order(request)
swap_id = order.id
await swap_service.mark_initiated(swap_id, SwapActionRequest(tx_hash="0x1"))
# Manually backdate the timelock to simulate expiration
order.source_timelock = int((datetime.utcnow() - timedelta(hours=1)).timestamp())
test_db.commit()
action_req = SwapActionRequest(tx_hash="0xRefund")
order = await swap_service.refund_swap(swap_id, action_req)
assert order.status == SwapStatus.REFUNDED

View File

@@ -0,0 +1,111 @@
import pytest
from unittest.mock import AsyncMock
from sqlmodel import Session, create_engine, SQLModel
from sqlmodel.pool import StaticPool
from app.services.wallet_service import WalletService
from app.domain.wallet import WalletType, NetworkType, NetworkConfig, TransactionStatus
from app.schemas.wallet import WalletCreate, TransactionRequest
@pytest.fixture
def test_db():
engine = create_engine(
"sqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
SQLModel.metadata.create_all(engine)
session = Session(engine)
yield session
session.close()
@pytest.fixture
def mock_contract_service():
return AsyncMock()
@pytest.fixture
def wallet_service(test_db, mock_contract_service):
# Setup some basic networks
network = NetworkConfig(
chain_id=1,
name="Ethereum",
network_type=NetworkType.EVM,
rpc_url="http://localhost:8545",
explorer_url="http://etherscan.io",
native_currency_symbol="ETH"
)
test_db.add(network)
test_db.commit()
return WalletService(session=test_db, contract_service=mock_contract_service)
@pytest.mark.asyncio
async def test_create_wallet(wallet_service):
request = WalletCreate(agent_id="agent-123", wallet_type=WalletType.EOA)
wallet = await wallet_service.create_wallet(request)
assert wallet.agent_id == "agent-123"
assert wallet.wallet_type == WalletType.EOA
assert wallet.address.startswith("0x")
assert wallet.is_active is True
@pytest.mark.asyncio
async def test_create_duplicate_wallet_fails(wallet_service):
request = WalletCreate(agent_id="agent-123", wallet_type=WalletType.EOA)
await wallet_service.create_wallet(request)
with pytest.raises(ValueError):
await wallet_service.create_wallet(request)
@pytest.mark.asyncio
async def test_get_wallet_by_agent(wallet_service):
await wallet_service.create_wallet(WalletCreate(agent_id="agent-123", wallet_type=WalletType.EOA))
await wallet_service.create_wallet(WalletCreate(agent_id="agent-123", wallet_type=WalletType.SMART_CONTRACT))
wallets = await wallet_service.get_wallet_by_agent("agent-123")
assert len(wallets) == 2
@pytest.mark.asyncio
async def test_update_balance(wallet_service):
wallet = await wallet_service.create_wallet(WalletCreate(agent_id="agent-123"))
balance = await wallet_service.update_balance(
wallet_id=wallet.id,
chain_id=1,
token_address="native",
balance=10.5
)
assert balance.balance == 10.5
assert balance.token_symbol == "ETH"
# Update existing
balance2 = await wallet_service.update_balance(
wallet_id=wallet.id,
chain_id=1,
token_address="native",
balance=20.0
)
assert balance2.id == balance.id
assert balance2.balance == 20.0
@pytest.mark.asyncio
async def test_submit_transaction(wallet_service):
wallet = await wallet_service.create_wallet(WalletCreate(agent_id="agent-123"))
tx_req = TransactionRequest(
chain_id=1,
to_address="0x1234567890123456789012345678901234567890",
value=1.5
)
tx = await wallet_service.submit_transaction(wallet.id, tx_req)
assert tx.wallet_id == wallet.id
assert tx.chain_id == 1
assert tx.to_address == tx_req.to_address
assert tx.value == 1.5
assert tx.status == TransactionStatus.SUBMITTED
assert tx.tx_hash is not None
assert tx.tx_hash.startswith("0x")