AgentSkillsCN

Bridge Core

Bridge.xyz API的核心编排与实用工具集。包含API客户端的初始化设置、常见模式、错误处理机制,以及各类集成辅助工具。适用于:初始化Bridge客户端、发起API请求、处理异常情况,以及在所有Bridge相关操作中通用的实用功能。

SKILL.md
--- frontmatter
name: Bridge Core
description: Core orchestration and utilities for Bridge.xyz API. API client setup, common patterns, error handling, and integration helpers. Use for: initializing Bridge client, making API requests, handling errors, and common utilities across all Bridge operations.

Bridge Core

API Client Setup

typescript
interface BridgeClientConfig {
  apiKey: string;
  baseUrl?: string;
  timeout?: number;
  idempotencyKey?: string;
}

class BridgeClient {
  private apiKey: string;
  private baseUrl: string;
  private timeout: number;

  constructor(config: BridgeClientConfig) {
    this.apiKey = config.apiKey;
    this.baseUrl = config.baseUrl || 'https://api.bridge.xyz/v0';
    this.timeout = config.timeout || 30000;
  }

  async request<T>(
    method: 'GET' | 'POST' | 'PUT' | 'DELETE',
    endpoint: string,
    options: {
      body?: Record<string, unknown>;
      query?: Record<string, string>;
      idempotencyKey?: string;
    } = {}
  ): Promise<T> {
    const url = new URL(`${this.baseUrl}${endpoint}`);
    
    if (options.query) {
      Object.entries(options.query).forEach(([key, value]) => {
        url.searchParams.set(key, value);
      });
    }

    const headers: Record<string, string> = {
      'Api-Key': this.apiKey,
      'Content-Type': 'application/json',
    };

    if (options.idempotencyKey) {
      headers['Idempotency-Key'] = options.idempotencyKey;
    }

    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), this.timeout);

    try {
      const response = await fetch(url.toString(), {
        method,
        headers,
        body: options.body ? JSON.stringify(options.body) : undefined,
        signal: controller.signal,
      });

      clearTimeout(timeout);

      if (!response.ok) {
        const error = await response.json();
        throw new BridgeApiError(error, response.status);
      }

      return response.json();
    } finally {
      clearTimeout(timeout);
    }
  }

  // Convenience methods
  get<T>(endpoint: string, query?: Record<string, string>): Promise<T> {
    return this.request<T>('GET', endpoint, { query });
  }

  post<T>(endpoint: string, body: Record<string, unknown>, idempotencyKey?: string): Promise<T> {
    return this.request<T>('POST', endpoint, { body, idempotencyKey });
  }

  put<T>(endpoint: string, body: Record<string, unknown>): Promise<T> {
    return this.request<T>('PUT', endpoint, { body });
  }

  delete<T>(endpoint: string): Promise<T> {
    return this.request<T>('DELETE', endpoint);
  }
}

Error Handling

typescript
interface BridgeApiErrorData {
  code: string;
  message: string;
  location?: string;
  name?: string;
  errors?: Array<{ code: string; message: string }>;
}

export class BridgeApiError extends Error {
  constructor(
    public data: BridgeApiErrorData,
    public status: number
  ) {
    super(data.message);
    this.name = 'BridgeApiError';
  }

  static fromResponse(data: unknown): BridgeApiError {
    const errorData = data as BridgeApiErrorData;
    return new BridgeApiError(errorData, 400);
  }

  get isAuthError(): boolean {
    return this.status === 401;
  }

  get isNotFound(): boolean {
    return this.status === 404;
  }

  get isServerError(): boolean {
    return this.status >= 500;
  }

  get isClientError(): boolean {
    return this.status >= 400 && this.status < 500;
  }
}

export class BridgeError extends Error {
  constructor(
    message: string,
    public code: string,
    public context?: Record<string, unknown>
  ) {
    super(message);
    this.name = 'BridgeError';
  }
}

Error Codes

typescript
const ERROR_CODES = {
  REQUIRED: 'required',
  INVALID: 'invalid',
  NOT_FOUND: 'not_found',
  UNAUTHORIZED: 'unauthorized',
  RATE_LIMITED: 'rate_limited',
  SERVER_ERROR: 'unexpected',
  VALIDATION_ERROR: 'bad_customer_request',
} as const;

function handleBridgeError(error: unknown): never {
  if (error instanceof BridgeApiError) {
    switch (error.code) {
      case ERROR_CODES.REQUIRED:
        throw new Error(`Missing required field: ${error.data.name}`);
      case ERROR_CODES.INVALID:
        throw new Error(`Invalid value: ${error.data.message}`);
      case ERROR_CODES.UNAUTHORIZED:
        throw new Error('Invalid or missing API key');
      case ERROR_CODES.NOT_FOUND:
        throw new Error(`Resource not found: ${error.data.message}`);
      default:
        throw new Error(`API error: ${error.data.message}`);
    }
  }
  
  if (error instanceof BridgeError) {
    throw error;
  }
  
  throw new Error(`Unexpected error: ${error}`);
}

Idempotency

typescript
import crypto from 'crypto';

function generateIdempotencyKey(): string {
  return crypto.randomUUID();
}

class IdempotencyManager {
  private pending = new Map<string, { promise: Promise<unknown>; timeout: NodeJS.Timeout }>();
  private cache = new Map<string, { result: unknown; timestamp: number }>();
  private readonly TTL = 24 * 60 * 60 * 1000; // 24 hours

  async withIdempotency<T>(
    key: string,
    operation: () => Promise<T>
  ): Promise<T> {
    // Check cache
    const cached = this.cache.get(key);
    if (cached && Date.now() - cached.timestamp < this.TTL) {
      return cached.result as T;
    }

    // Check pending
    const pending = this.pending.get(key);
    if (pending) {
      return pending.promise as Promise<T>;
    }

    // Execute operation
    const promise = operation()
      .then(result => {
        this.cache.set(key, { result, timestamp: Date.now() });
        this.pending.delete(key);
        return result;
      })
      .catch(error => {
        this.pending.delete(key);
        throw error;
      });

    this.pending.set(key, { promise, timeout: setTimeout(() => {
      this.pending.delete(key);
    }, 60000) });

    return promise;
  }

  clearCache(): void {
    this.cache.clear();
  }
}

Validation Utilities

typescript
// Address validation by chain
function isValidAddress(address: string, chain: string): boolean {
  switch (chain) {
    case 'ethereum':
    case 'polygon':
    case 'base':
    case 'arbitrum':
      return /^0x[a-fA-F0-9]{40}$/.test(address);
    case 'solana':
      return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address);
    case 'tron':
      return /^T[a-zA-Z0-9]{33}$/.test(address);
    default:
      return false;
  }
}

// Currency validation
const VALID_CURRENCIES = ['usd', 'eur', 'mxn', 'usdc', 'usdb', 'usdt'] as const;
type Currency = typeof VALID_CURRENCIES[number];

function isValidCurrency(currency: string): currency is Currency {
  return VALID_CURRENCIES.includes(currency as Currency);
}

// Amount validation
function isValidAmount(amount: string): boolean {
  const num = parseFloat(amount);
  return !isNaN(num) && num > 0 && Number.isFinite(num);
}

// Chain validation
const VALID_CHAINS = [
  'ethereum', 'solana', 'polygon', 'base', 'arbitrum', 'tron'
] as const;

function isValidChain(chain: string): chain is typeof VALID_CHAINS[number] {
  return VALID_CHAINS.includes(chain as typeof VALID_CHAINS[number]);
}

// Comprehensive request validation
interface ValidatedRequest<T> {
  valid: boolean;
  errors: string[];
  data?: T;
}

function validateTransferRequest(
  request: Record<string, unknown>
): ValidatedRequest<CreateTransferRequest> {
  const errors: string[] = [];
  
  if (!request.amount || !isValidAmount(request.amount as string)) {
    errors.push('Invalid amount');
  }
  
  if (!request.source) {
    errors.push('Missing source');
  } else {
    const source = request.source as Record<string, unknown>;
    if (!source.payment_rail || !isValidChain(source.payment_rail as string)) {
      errors.push('Invalid source chain');
    }
    if (!source.currency || !isValidCurrency(source.currency as string)) {
      errors.push('Invalid source currency');
    }
  }
  
  if (!request.destination) {
    errors.push('Missing destination');
  } else {
    const dest = request.destination as Record<string, unknown>;
    if (!dest.payment_rail || !isValidChain(dest.payment_rail as string)) {
      errors.push('Invalid destination chain');
    }
    if (!dest.to_address && !dest.external_account_id) {
      errors.push('Missing destination address or account ID');
    }
  }
  
  if (errors.length > 0) {
    return { valid: false, errors };
  }
  
  return { valid: true, errors: [], data: request as CreateTransferRequest };
}

Rate Limiting

typescript
class RateLimiter {
  private requests: number[] = [];
  private readonly maxRequests: number;
  private readonly windowMs: number;

  constructor(maxRequests: number = 100, windowMs: number = 60000) {
    this.maxRequests = maxRequests;
    this.windowMs = windowMs;
  }

  async acquire(): Promise<void> {
    const now = Date.now();
    
    // Remove old requests outside window
    this.requests = this.requests.filter(t => now - t < this.windowMs);
    
    if (this.requests.length >= this.maxRequests) {
      const oldest = this.requests[0];
      const waitTime = this.windowMs - (now - oldest);
      await new Promise(resolve => setTimeout(resolve, waitTime));
      return this.acquire();
    }
    
    this.requests.push(now);
  }
}

Complete Client Example

typescript
class Bridge extends BridgeClient {
  customers = {
    createKycLink: (data: CreateKycLinkRequest) =>
      this.post<KycLinkResponse>('/kyc_links', data, generateIdempotencyKey()),
    list: () => this.get<Customer[]>('/customers'),
    get: (id: string) => this.get<Customer>(`/customers/${id}`),
  };

  wallets = {
    create: (customerId: string, chain: string) =>
      this.post<Wallet>(`/customers/${customerId}/wallets`, { chain }, generateIdempotencyKey()),
    list: (customerId: string) => this.get<Wallet[]>(`/customers/${customerId}/wallets`),
    get: (id: string) => this.get<Wallet>(`/wallets/${id}`),
  };

  transfers = {
    create: (data: CreateTransferRequest) =>
      this.post<Transfer>('/transfers', data, generateIdempotencyKey()),
    list: (params?: ListTransfersParams) =>
      this.get<{ count: number; data: Transfer[] }>('/transfers', params),
    get: (id: string) => this.get<Transfer>(`/transfers/${id}`),
  };

  virtualAccounts = {
    create: (customerId: string, data: CreateVirtualAccountRequest) =>
      this.post<VirtualAccount>(`/customers/${customerId}/virtual_accounts`, data, generateIdempotencyKey()),
    list: (customerId: string) =>
      this.get<VirtualAccount[]>(`/customers/${customerId}/virtual_accounts`),
  };

  cards = {
    create: (customerId: string, data: CreateCardRequest) =>
      this.post<CardAccount>(`/customers/${customerId}/card_accounts`, data, generateIdempotencyKey()),
    list: (customerId: string) =>
      this.get<CardAccount[]>(`/customers/${customerId}/card_accounts`),
    get: (id: string) => this.get<CardAccount>(`/card_accounts/${id}`),
    freeze: (id: string, reason: string) =>
      this.post<CardAccount>(`/card_accounts/${id}/freezes`, { reason }),
  };

  externalAccounts = {
    create: (data: CreateExternalAccountRequest) =>
      this.post<ExternalAccount>('/external_accounts', data, generateIdempotencyKey()),
    list: (customerId: string) =>
      this.get<ExternalAccount[]>(`/customers/${customerId}/external_accounts`),
    delete: (id: string) => this.delete(`/external_accounts/${id}`),
  };

  webhooks = {
    list: () => this.get<{ data: Webhook[] }>('/webhooks'),
    create: (url: string) => this.post<Webhook>('/webhooks', { url }, generateIdempotencyKey()),
    delete: (id: string) => this.delete(`/webhooks/${id}`),
  };

  fees = {
    updateDefault: (percent: string) =>
      this.post<{ summary: string }>('/developer/fees', { default_liquidation_address_fee_percent: percent }),
  };
}

// Initialize client
export const bridge = new Bridge({
  apiKey: process.env.BRIDGE_API_KEY!,
  timeout: 30000,
});

// Usage
async function main() {
  try {
    // Create customer KYC link
    const kycLink = await bridge.customers.createKycLink({
      full_name: 'John Doe',
      email: 'john@example.com',
      type: 'individual',
    });

    // Create wallet
    const wallet = await bridge.wallets.create(kycLink.customer_id, 'solana');

    // Create transfer
    const transfer = await bridge.transfers.create({
      amount: '100',
      on_behalf_of: kycLink.customer_id,
      source: { payment_rail: 'ach_push', currency: 'usd' },
      destination: { payment_rail: 'solana', currency: 'usdc', to_address: wallet.address },
    });

    console.log('Transfer created:', transfer.id);
  } catch (error) {
    handleBridgeError(error);
  }
}

Constants Reference

typescript
// Payment Rails
const PAYMENT_RAILS = {
  fiat: {
    us: ['ach_push', 'ach', 'wire'] as const,
    eu: ['sepa'] as const,
    mx: ['spei'] as const,
    br: ['pix'] as const,
  },
  crypto: ['ethereum', 'solana', 'polygon', 'base', 'arbitrum', 'tron'] as const,
  bridge: ['bridge_wallet', 'card'] as const,
} as const;

// Currencies
const CURRENCIES = ['usd', 'eur', 'mxn', 'usdc', 'usdb', 'usdt'] as const;

// Transfer States
const TRANSFER_STATES = ['awaiting_funds', 'completed', 'failed'] as const;

// Customer Statuses
const CUSTOMER_STATUSES = ['active', 'pending', 'suspended'] as const;

// Card Statuses
const CARD_STATUSES = ['active', 'pending', 'inactive', 'frozen'] as const;

Best Practices

  1. Use typed client - Leverage TypeScript for type safety
  2. Handle errors - Always catch and handle BridgeApiError
  3. Use idempotency - Prevent duplicate operations
  4. Validate inputs - Use validation utilities before requests
  5. Rate limit - Implement rate limiting for production
  6. Environment variables - Store API keys securely