Перейти к содержанию

Protocol Integration Architecture

Обзор

Technical documentation для интеграции Saga DeFi Platform с blockchain protocols, smart contracts и external systems.

Integration Architecture

┌─────────────────────────────────────────────────────────────┐
│                  Saga DeFi Platform                          │
│  ┌────────────────────────────────────────────────────┐    │
│  │         Backend Go Services                        │    │
│  │         - FaucetService                            │    │
│  │         - BalanceService                           │    │
│  │         - InvestmentService                        │    │
│  │         - WithdrawalService                        │    │
│  └────────────────────────────────────────────────────┘    │
│                          ↓                                  │
│  ┌────────────────────────────────────────────────────┐    │
│  │         Blockchain Integration Layer               │    │
│  │         - ethclient (go-ethereum)                  │    │
│  │         - Contract ABIs (generated from Solidity)  │    │
│  └────────────────────────────────────────────────────┘    │
└──────────────────────────┬───────────────────────────────────┘
                           │ RPC Calls
┌─────────────────────────────────────────────────────────────┐
│                  VPS Blockchain Node                         │
│                  Anvil (Foundry)                             │
│                  http://188.42.218.164:8545                  │
│                  Network ID: 1337                            │
│                                                              │
│  ┌────────────────────────────────────────────────────┐    │
│  │         Smart Contracts (UUPS Upgradeable)         │    │
│  │         - TestUSDC (ERC20)                         │    │
│  │         - StakingProtocol5/10/20                   │    │
│  │         - stUSDC5/10/20 (ERC20 tokens)            │    │
│  └────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

Core Protocol Integrations

1. Ethereum JSON-RPC Integration

Go-Ethereum Client:

// backend/shared/services/blockchain_client.go
import "github.com/ethereum/go-ethereum/ethclient"

type BlockchainClient struct {
    client *ethclient.Client
    config *config.UnifiedConfig
}

func NewBlockchainClient(cfg *config.UnifiedConfig) (*BlockchainClient, error) {
    // Connect to VPS blockchain node
    client, err := ethclient.Dial(cfg.GetBlockchainRPCURL())
    if err != nil {
        return nil, fmt.Errorf("failed to connect to blockchain: %w", err)
    }

    return &BlockchainClient{
        client: client,
        config: cfg,
    }, nil
}

// Get latest block number
func (bc *BlockchainClient) GetBlockNumber(ctx context.Context) (uint64, error) {
    return bc.client.BlockNumber(ctx)
}

// Get chain ID (1337 for Saga Testnet)
func (bc *BlockchainClient) GetChainID(ctx context.Context) (*big.Int, error) {
    return bc.client.ChainID(ctx)
}

Configuration:

# config/blockchain.yaml
blockchain:
  rpc_url: "http://188.42.218.164:8545"
  network_id: 1337
  network_name: "Saga Testnet"
  block_time: 12  # seconds
  confirmations_required: 1  # testnet

2. ERC20 Token Integration (USDC)

ABI Generation:

# Generate Go bindings from Solidity contracts
cd blockchain/contracts
forge build

# Generate Go contract bindings
abigen --abi out/TestUSDC.sol/TestUSDC.json --pkg contracts --type TestUSDC --out ../../backend/blockchain/contracts/TestUSDC.go

USDC Contract Integration:

// backend/shared/services/usdc_service.go
import (
    "github.com/ethereum/go-ethereum/accounts/abi/bind"
    "github.com/ethereum/go-ethereum/common"
    "github.com/aiseeq/saga/backend/blockchain/contracts"
)

type USDCService struct {
    client  *ethclient.Client
    contract *contracts.TestUSDC
    config  *config.UnifiedConfig
}

func NewUSDCService(client *ethclient.Client, cfg *config.UnifiedConfig) (*USDCService, error) {
    // Load USDC contract address from config
    usdcAddress := common.HexToAddress(cfg.GetUSDCContractAddress())

    // Initialize contract instance
    usdc, err := contracts.NewTestUSDC(usdcAddress, client)
    if err != nil {
        return nil, err
    }

    return &USDCService{
        client:  client,
        contract: usdc,
        config:  cfg,
    }, nil
}

// Get USDC balance of address
func (us *USDCService) GetBalance(ctx context.Context, address string) (SafeDecimal, error) {
    addr := common.HexToAddress(address)

    balance, err := us.contract.BalanceOf(&bind.CallOpts{Context: ctx}, addr)
    if err != nil {
        return SafeDecimal{}, err
    }

    // Convert from big.Int to SafeDecimal (6 decimals for USDC)
    return SafeDecimalFromBigInt(balance, 6), nil
}

// Transfer USDC from master wallet to user
func (us *USDCService) Transfer(ctx context.Context, to string, amount SafeDecimal) (string, error) {
    auth, err := us.getTransactionAuth(ctx)
    if err != nil {
        return "", err
    }

    toAddr := common.HexToAddress(to)
    amountBigInt := amount.ToBigInt()

    tx, err := us.contract.Transfer(auth, toAddr, amountBigInt)
    if err != nil {
        return "", err
    }

    // Wait for transaction confirmation
    receipt, err := bind.WaitMined(ctx, us.client, tx)
    if err != nil {
        return "", err
    }

    if receipt.Status != types.ReceiptStatusSuccessful {
        return "", errors.New("transaction failed")
    }

    return tx.Hash().Hex(), nil
}

3. Staking Protocol Integration

StakingProtocol Contract Interface:

// backend/shared/services/staking_protocol_service.go
type StakingProtocolService struct {
    client   *ethclient.Client
    config   *config.UnifiedConfig
    contracts map[int]*contracts.StakingProtocol  // strategyID → contract
}

func NewStakingProtocolService(client *ethclient.Client, cfg *config.UnifiedConfig) (*StakingProtocolService, error) {
    s := &StakingProtocolService{
        client:   client,
        config:   cfg,
        contracts: make(map[int]*contracts.StakingProtocol),
    }

    // Initialize contract instances for each strategy
    for _, strategy := range cfg.GetAllStrategies() {
        address := common.HexToAddress(strategy.ContractAddress)
        contract, err := contracts.NewStakingProtocol(address, client)
        if err != nil {
            return nil, fmt.Errorf("failed to load strategy %d contract: %w", strategy.ID, err)
        }
        s.contracts[strategy.ID] = contract
    }

    return s, nil
}

// Stake USDC in protocol
func (s *StakingProtocolService) Stake(ctx context.Context, strategyID int, userAddress string, amount SafeDecimal) (string, error) {
    contract := s.contracts[strategyID]
    if contract == nil {
        return "", errors.New("invalid strategy ID")
    }

    auth, err := s.getTransactionAuth(ctx, userAddress)
    if err != nil {
        return "", err
    }

    amountBigInt := amount.ToBigInt()

    // Call stake() function
    tx, err := contract.Stake(auth, amountBigInt)
    if err != nil {
        return "", err
    }

    // Wait for confirmation
    receipt, err := bind.WaitMined(ctx, s.client, tx)
    if err != nil {
        return "", err
    }

    if receipt.Status != types.ReceiptStatusSuccessful {
        return "", errors.New("staking transaction failed")
    }

    return tx.Hash().Hex(), nil
}

// Unstake and return USDC
func (s *StakingProtocolService) Unstake(ctx context.Context, strategyID int, userAddress string, stAmount SafeDecimal) (string, error) {
    contract := s.contracts[strategyID]
    if contract == nil {
        return "", errors.New("invalid strategy ID")
    }

    auth, err := s.getTransactionAuth(ctx, userAddress)
    if err != nil {
        return "", err
    }

    // stTokens have 18 decimals
    stAmountBigInt := stAmount.ToBigInt()

    // Call unstake() function
    tx, err := contract.Unstake(auth, stAmountBigInt)
    if err != nil {
        return "", err
    }

    // Wait for confirmation
    receipt, err := bind.WaitMined(ctx, s.client, tx)
    if err != nil {
        return "", err
    }

    if receipt.Status != types.ReceiptStatusSuccessful {
        return "", errors.New("unstaking transaction failed")
    }

    return tx.Hash().Hex(), nil
}

// Get protocol statistics
func (s *StakingProtocolService) GetProtocolStats(ctx context.Context, strategyID int) (*ProtocolStats, error) {
    contract := s.contracts[strategyID]
    if contract == nil {
        return nil, errors.New("invalid strategy ID")
    }

    // Query total staked
    totalStaked, err := contract.GetTotalStaked(&bind.CallOpts{Context: ctx})
    if err != nil {
        return nil, err
    }

    // Query APY
    apy, err := contract.GetAPY(&bind.CallOpts{Context: ctx})
    if err != nil {
        return nil, err
    }

    // Query exchange rate
    exchangeRate, err := contract.GetExchangeRate(&bind.CallOpts{Context: ctx})
    if err != nil {
        return nil, err
    }

    return &ProtocolStats{
        TotalStaked:  SafeDecimalFromBigInt(totalStaked, 6),
        APY:          apy.Uint64(),
        ExchangeRate: SafeDecimalFromBigInt(exchangeRate, 18),
    }, nil
}

4. stToken Integration

stToken (ERC20) Service:

// backend/shared/services/st_token_service.go
type StTokenService struct {
    client   *ethclient.Client
    config   *config.UnifiedConfig
    contracts map[int]*contracts.StToken  // strategyID → stToken contract
}

func NewStTokenService(client *ethclient.Client, cfg *config.UnifiedConfig) (*StTokenService, error) {
    s := &StTokenService{
        client:   client,
        config:   cfg,
        contracts: make(map[int]*contracts.StToken),
    }

    // Initialize stToken contracts for each strategy
    for _, strategy := range cfg.GetAllStrategies() {
        address := common.HexToAddress(strategy.StTokenAddress)
        contract, err := contracts.NewStToken(address, client)
        if err != nil {
            return nil, fmt.Errorf("failed to load stToken for strategy %d: %w", strategy.ID, err)
        }
        s.contracts[strategy.ID] = contract
    }

    return s, nil
}

// Get user's stToken balance
func (s *StTokenService) GetBalance(ctx context.Context, strategyID int, userAddress string) (SafeDecimal, error) {
    contract := s.contracts[strategyID]
    if contract == nil {
        return SafeDecimal{}, errors.New("invalid strategy ID")
    }

    addr := common.HexToAddress(userAddress)

    balance, err := contract.BalanceOf(&bind.CallOpts{Context: ctx}, addr)
    if err != nil {
        return SafeDecimal{}, err
    }

    // stTokens have 18 decimals
    return SafeDecimalFromBigInt(balance, 18), nil
}

// Get exchange rate (stToken → USDC)
func (s *StTokenService) GetExchangeRate(ctx context.Context, strategyID int) (SafeDecimal, error) {
    contract := s.contracts[strategyID]
    if contract == nil {
        return SafeDecimal{}, errors.New("invalid strategy ID")
    }

    rate, err := contract.ExchangeRate(&bind.CallOpts{Context: ctx})
    if err != nil {
        return SafeDecimal{}, err
    }

    return SafeDecimalFromBigInt(rate, 18), nil
}

// Calculate current USDC value of stTokens
func (s *StTokenService) CalculateUSDCValue(ctx context.Context, strategyID int, stTokenAmount SafeDecimal) (SafeDecimal, error) {
    exchangeRate, err := s.GetExchangeRate(ctx, strategyID)
    if err != nil {
        return SafeDecimal{}, err
    }

    // USDC value = stTokenAmount * exchangeRate
    return stTokenAmount.Mul(exchangeRate), nil
}

Transaction Management

Transaction Authorization

// backend/shared/services/transaction_auth.go
type TransactionAuth struct {
    client     *ethclient.Client
    config     *config.UnifiedConfig
    privateKey *ecdsa.PrivateKey
}

func NewTransactionAuth(client *ethclient.Client, cfg *config.UnifiedConfig) (*TransactionAuth, error) {
    // Load private key from environment (NEVER hardcode!)
    privateKeyHex := cfg.GetHDWalletPrivateKey()
    privateKey, err := crypto.HexToECDSA(strings.TrimPrefix(privateKeyHex, "0x"))
    if err != nil {
        return nil, fmt.Errorf("failed to load private key: %w", err)
    }

    return &TransactionAuth{
        client:     client,
        config:     cfg,
        privateKey: privateKey,
    }, nil
}

// Get transaction auth options
func (ta *TransactionAuth) GetAuth(ctx context.Context) (*bind.TransactOpts, error) {
    // Get chain ID
    chainID, err := ta.client.ChainID(ctx)
    if err != nil {
        return nil, err
    }

    // Create transactor
    auth, err := bind.NewKeyedTransactorWithChainID(ta.privateKey, chainID)
    if err != nil {
        return nil, err
    }

    // Get nonce
    publicKey := ta.privateKey.Public()
    publicKeyECDSA, _ := publicKey.(*ecdsa.PublicKey)
    fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)

    nonce, err := ta.client.PendingNonceAt(ctx, fromAddress)
    if err != nil {
        return nil, err
    }

    auth.Nonce = big.NewInt(int64(nonce))

    // Gas price
    gasPrice, err := ta.client.SuggestGasPrice(ctx)
    if err != nil {
        return nil, err
    }

    auth.GasPrice = gasPrice
    auth.GasLimit = uint64(300000) // Default gas limit

    return auth, nil
}

Transaction Monitoring

// backend/shared/services/transaction_monitor.go
type TransactionMonitor struct {
    client *ethclient.Client
    logger *logging.CanonicalLogger
}

// Wait for transaction confirmation with timeout
func (tm *TransactionMonitor) WaitForConfirmation(ctx context.Context, txHash common.Hash, timeout time.Duration) (*types.Receipt, error) {
    ctx, cancel := context.WithTimeout(ctx, timeout)
    defer cancel()

    ticker := time.NewTicker(2 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            return nil, errors.New("transaction confirmation timeout")

        case <-ticker.C:
            receipt, err := tm.client.TransactionReceipt(ctx, txHash)
            if err == nil {
                if receipt.Status == types.ReceiptStatusSuccessful {
                    tm.logger.InfoStructured("Transaction confirmed",
                        "tx_hash", txHash.Hex(),
                        "block_number", receipt.BlockNumber.Uint64(),
                    )
                    return receipt, nil
                } else {
                    return nil, errors.New("transaction failed")
                }
            }

            // Transaction not yet mined, continue waiting
            tm.logger.DebugStructured("Waiting for transaction",
                "tx_hash", txHash.Hex(),
            )
        }
    }
}

Testing Protocol Integration

Integration Test Example

// backend/tests/integration/protocol_integration_test.go
func TestProtocolIntegration(t *testing.T) {
    // Setup
    cfg := loadTestConfig()
    client, err := ethclient.Dial(cfg.GetBlockchainRPCURL())
    require.NoError(t, err)

    // Initialize services
    usdcService, err := NewUSDCService(client, cfg)
    require.NoError(t, err)

    stakingService, err := NewStakingProtocolService(client, cfg)
    require.NoError(t, err)

    stTokenService, err := NewStTokenService(client, cfg)
    require.NoError(t, err)

    // Test scenario: Stake → Check stTokens → Unstake
    userAddress := "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"
    amount := SafeDecimalFromString("100.00")

    // 1. Check initial USDC balance
    initialBalance, err := usdcService.GetBalance(context.Background(), userAddress)
    require.NoError(t, err)
    t.Logf("Initial USDC balance: %s", initialBalance.String())

    // 2. Stake USDC (Conservative strategy, ID=1)
    stakeTxHash, err := stakingService.Stake(context.Background(), 1, userAddress, amount)
    require.NoError(t, err)
    t.Logf("Stake tx hash: %s", stakeTxHash)

    // 3. Check stToken balance
    time.Sleep(5 * time.Second) // Wait for transaction confirmation
    stBalance, err := stTokenService.GetBalance(context.Background(), 1, userAddress)
    require.NoError(t, err)
    t.Logf("stToken balance: %s", stBalance.String())
    assert.True(t, stBalance.GreaterThan(SafeDecimalFromString("0")))

    // 4. Get exchange rate
    exchangeRate, err := stTokenService.GetExchangeRate(context.Background(), 1)
    require.NoError(t, err)
    t.Logf("Exchange rate: %s", exchangeRate.String())

    // 5. Calculate current USDC value
    usdcValue, err := stTokenService.CalculateUSDCValue(context.Background(), 1, stBalance)
    require.NoError(t, err)
    t.Logf("Current USDC value: %s", usdcValue.String())

    // 6. Unstake
    unstakeTxHash, err := stakingService.Unstake(context.Background(), 1, userAddress, stBalance)
    require.NoError(t, err)
    t.Logf("Unstake tx hash: %s", unstakeTxHash)

    // 7. Verify final balance
    time.Sleep(5 * time.Second)
    finalBalance, err := usdcService.GetBalance(context.Background(), userAddress)
    require.NoError(t, err)
    t.Logf("Final USDC balance: %s", finalBalance.String())
}

Monitoring & Observability

Protocol Health Check:

// GET /api/health
func (h *HealthHandler) GetHealth(w http.ResponseWriter, r *http.Request) {
    health := HealthResponse{
        Status:    "healthy",
        Version:   h.config.GetVersion(),
        Timestamp: time.Now().UTC(),
    }

    // Check blockchain connectivity
    client, err := ethclient.Dial(h.config.GetBlockchainRPCURL())
    if err != nil {
        health.Blockchain = "disconnected"
        health.Status = "degraded"
    } else {
        blockNumber, err := client.BlockNumber(context.Background())
        if err == nil {
            health.Blockchain = fmt.Sprintf("connected (block: %d)", blockNumber)
        } else {
            health.Blockchain = "connected (query failed)"
            health.Status = "degraded"
        }
    }

    // Check database connectivity
    if err := h.db.Ping(); err != nil {
        health.Database = "disconnected"
        health.Status = "unhealthy"
    } else {
        health.Database = "connected"
    }

    http_helpers.SendSuccess(w, health)
}



📋 Метаданные

Версия: 2.4.82

Обновлено: 2025-10-21

Статус: Published