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())
}
Related Documentation¶
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