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

Custody Integration Guide

Custody Solutions Overview

Supported Custody Types:

1. Self-Custody (Current) - User controls private keys через MetaMask - Best for: Retail users, DeFi enthusiasts - Security: User responsibility

2. Custodial Wallets (Planned) - Saga manages private keys - Best for: Less technical users - Security: Platform responsibility

3. Institutional Custody (Future) - Third-party professional custody (Fireblocks, Copper, BitGo) - Best for: Institutions, large accounts - Security: Multi-signature, insurance

Self-Custody Architecture (Current)

Web3 Wallet Integration:

MetaMask Connection:

// frontend/user-app/src/lib/web3-auth-context.tsx
import { useConnect, useAccount } from 'wagmi';

export function connectWallet() {
  const { connect, connectors } = useConnect();
  const { address } = useAccount();

  // Connect to MetaMask
  const metaMask = connectors.find(c => c.id === 'metaMask');
  if (metaMask) {
    connect({ connector: metaMask });
  }

  return address;
}

Signature-based Authentication:

// User signs message with private key
const message = `Authenticate to Saga Platform\nNonce: ${nonce}`;
const signature = await wallet.signMessage(message);

// Backend verifies signature
const recoveredAddress = ethers.verifyMessage(message, signature);
if (recoveredAddress.toLowerCase() === walletAddress.toLowerCase()) {
  // Authentication successful
}

Security Considerations:

  • ✅ User retains full control of assets
  • ✅ No private keys stored on Saga servers
  • ✅ Non-custodial architecture (regulatory advantage)
  • ⚠️ User responsible for key security

🏢 Institutional Custody Integration

Fireblocks Integration (Planned)

Architecture:

User → Saga Platform → Fireblocks API → Fireblocks Vault
              Multi-Signature
              Approval Workflow
              Blockchain Transaction

Implementation Example:

// backend/custody/fireblocks/client.go
type FireblocksClient struct {
    apiKey    string
    apiSecret string
    baseURL   string
}

func (c *FireblocksClient) CreateVaultAccount(userID int64) (*VaultAccount, error) {
    // Create vault account for user
    req := &CreateVaultRequest{
        Name:       fmt.Sprintf("saga-user-%d", userID),
        HiddenOnUI: false,
        AutoFuel:   true,
    }

    resp, err := c.post("/v1/vault/accounts", req)
    if err != nil {
        return nil, err
    }

    return parseVaultAccount(resp), nil
}

func (c *FireblocksClient) InitiateWithdrawal(
    vaultID string,
    asset string,
    amount *big.Int,
    destination string,
) (*Transaction, error) {
    req := &TransactionRequest{
        AssetID:     asset,
        Source:      VaultAccountSource{Type: "VAULT_ACCOUNT", ID: vaultID},
        Destination: ExternalWalletDestination{Type: "EXTERNAL_WALLET", Address: destination},
        Amount:      amount.String(),
    }

    // Initiate transaction (requires approval)
    txResp, err := c.post("/v1/transactions", req)
    if err != nil {
        return nil, err
    }

    return parseTransaction(txResp), nil
}

Multi-Signature Approval:

type ApprovalWorkflow struct {
    MinApprovers int
    Approvers    []string
    Policy       *Policy
}

func (w *ApprovalWorkflow) RequiresApproval(amount *big.Int) bool {
    // Amounts > $10,000 require 2-of-3 approval
    threshold := big.NewInt(10000)
    return amount.Cmp(threshold) > 0
}

func (w *ApprovalWorkflow) GetApprovers(amount *big.Int) []string {
    if w.RequiresApproval(amount) {
        return w.Approvers  // Return all approvers for 2-of-3
    }
    return []string{}  // Auto-approve small amounts
}

Copper Integration (Alternative)

Copper ClearLoop API:

// backend/custody/copper/client.go
type CopperClient struct {
    apiKey     string
    apiSecret  string
    networkID  string
}

func (c *CopperClient) CreateWallet(userID int64, currency string) (*Wallet, error) {
    req := &CreateWalletRequest{
        UserID:    userID,
        Currency:  currency,
        NetworkID: c.networkID,
    }

    resp, err := c.post("/wallets", req)
    if err != nil {
        return nil, err
    }

    return parseWallet(resp), nil
}

func (c *CopperClient) RequestWithdrawal(
    walletID string,
    amount string,
    destination string,
) (*WithdrawalRequest, error) {
    req := &WithdrawalRequest{
        WalletID:    walletID,
        Amount:      amount,
        Destination: destination,
        Priority:    "NORMAL",
    }

    // Submit for approval
    resp, err := c.post("/withdrawals", req)
    if err != nil {
        return nil, err
    }

    return parseWithdrawalRequest(resp), nil
}

💼 Custodial Wallet Architecture

Saga-Managed Wallets (Planned)

Key Management:

// backend/custody/internal/key_manager.go
type KeyManager struct {
    kms    *awskms.Client  // AWS KMS for key storage
    region string
}

func (km *KeyManager) GenerateWallet(userID int64) (*Wallet, error) {
    // Generate key pair in AWS KMS
    keyResp, err := km.kms.CreateKey(context.Background(), &awskms.CreateKeyInput{
        Description: aws.String(fmt.Sprintf("saga-user-%d", userID)),
        KeyUsage:    types.KeyUsageTypeSignVerify,
        KeySpec:     types.KeySpecEccSecgP256k1,  // Ethereum compatible
    })
    if err != nil {
        return nil, err
    }

    // Derive Ethereum address from public key
    publicKey, err := km.getPublicKey(keyResp.KeyMetadata.KeyId)
    if err != nil {
        return nil, err
    }

    address := crypto.PubkeyToAddress(*publicKey).Hex()

    return &Wallet{
        UserID:  userID,
        Address: address,
        KeyID:   *keyResp.KeyMetadata.KeyId,
    }, nil
}

func (km *KeyManager) SignTransaction(keyID string, txHash []byte) ([]byte, error) {
    // Sign transaction using KMS
    signResp, err := km.kms.Sign(context.Background(), &awskms.SignInput{
        KeyId:            aws.String(keyID),
        Message:          txHash,
        MessageType:      types.MessageTypeDigest,
        SigningAlgorithm: types.SigningAlgorithmSpecEcdsaSha256,
    })
    if err != nil {
        return nil, err
    }

    return signResp.Signature, nil
}

Security Architecture:

  • ✅ Private keys NEVER leave AWS KMS
  • ✅ Multi-region replication
  • ✅ Audit logging (CloudTrail)
  • ✅ Role-based access control
  • ✅ Hardware security modules (HSM)

Withdrawal Flow with Custodial Wallets:

1. User Request → Saga Backend
2. Saga Backend → AWS KMS (sign transaction)
3. AWS KMS → Signed Transaction
4. Saga Backend → Blockchain
5. Blockchain → Confirmation
6. Saga Backend → User Notification

Migration Path

Phase 1: Self-Custody (Current)

  • ✅ MetaMask integration
  • ✅ Signature-based auth
  • ✅ User controls keys

Phase 2: Custodial Option (Q2 2025)

  • 🔄 AWS KMS integration
  • 🔄 Saga-managed wallets
  • 🔄 User choice: self-custody vs custodial

Phase 3: Institutional Custody (Q3 2025)

  • 📅 Fireblocks/Copper integration
  • 📅 Multi-signature workflows
  • 📅 Insurance coverage
  • 📅 Institutional grade security

API Integration Examples

Create Custodial Account:

POST /api/custody/accounts
Authorization: Bearer <jwt_token>
Content-Type: application/json

{
  "custody_type": "fireblocks",
  "user_id": 123,
  "asset": "USDC"
}

Response:
{
  "account_id": "fb-vault-456",
  "address": "0x742d35Cc9Cf3C4C3a3F5d7B5f",
  "custody_provider": "fireblocks",
  "created_at": "2025-10-06T12:00:00Z"
}

Initiate Custodial Withdrawal:

POST /api/custody/withdrawals
Authorization: Bearer <jwt_token>
Content-Type: application/json

{
  "account_id": "fb-vault-456",
  "amount": "1000.00",
  "asset": "USDC",
  "destination": "0xE4B5F7D8C9A2B1C3D4E5F6G7H8"
}

Response:
{
  "withdrawal_id": "wd-789",
  "status": "pending_approval",
  "approvers_required": 2,
  "approved_by": [],
  "estimated_completion": "2025-10-06T14:00:00Z"
}

Check Approval Status:

GET /api/custody/withdrawals/wd-789/status
Authorization: Bearer <jwt_token>

Response:
{
  "withdrawal_id": "wd-789",
  "status": "approved",
  "approved_by": ["admin1@saga.com", "admin2@saga.com"],
  "transaction_hash": "0xabc123...",
  "blockchain_confirmations": 12
}

Compliance and Reporting

Audit Trail:

type CustodyAuditLog struct {
    ID          int64     `json:"id"`
    UserID      int64     `json:"user_id"`
    Action      string    `json:"action"`
    CustodyType string    `json:"custody_type"`
    Amount      SafeDecimal `json:"amount"`
    Status      string    `json:"status"`
    Approvers   []string  `json:"approvers,omitempty"`
    CreatedAt   time.Time `json:"created_at"`
}

func (s *CustodyService) LogAction(log *CustodyAuditLog) error {
    // Insert audit log
    _, err := s.db.Exec(`
        INSERT INTO custody_audit_logs (
            user_id, action, custody_type, amount, status, approvers, created_at
        ) VALUES ($1, $2, $3, $4, $5, $6, $7)
    `, log.UserID, log.Action, log.CustodyType, log.Amount, log.Status, pq.Array(log.Approvers), log.CreatedAt)

    return err
}

Regulatory Reporting:

func (s *CustodyService) GenerateComplianceReport(startDate, endDate time.Time) (*ComplianceReport, error) {
    rows, err := s.db.Query(`
        SELECT
            custody_type,
            COUNT(*) as transaction_count,
            SUM(amount) as total_volume,
            AVG(amount) as average_amount
        FROM custody_audit_logs
        WHERE created_at BETWEEN $1 AND $2
        GROUP BY custody_type
    `, startDate, endDate)

    if err != nil {
        return nil, err
    }
    defer rows.Close()

    report := &ComplianceReport{
        Period:    fmt.Sprintf("%s to %s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02")),
        Providers: make([]*ProviderReport, 0),
    }

    for rows.Next() {
        var pr ProviderReport
        if err := rows.Scan(&pr.CustodyType, &pr.TransactionCount, &pr.TotalVolume, &pr.AverageAmount); err != nil {
            return nil, err
        }
        report.Providers = append(report.Providers, &pr)
    }

    return report, nil
}



📋 Метаданные

Версия: 2.4.82

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

Статус: Published