Overview

The route policy system provides a lightweight way to determine which asset pairs can be traded without requiring individual API calls for each route validation. Instead of fetching all possible routes from the server, you can download a compact policy configuration and compute valid routes locally.

This approach significantly reduces API calls and enables real-time route validation in your application.

How it works

Route policies use a priority-based validation system with four key components:

  1. Isolation Groups (Highest Priority): Assets that can ONLY trade with each other.
  2. Whitelist Overrides: Explicit exceptions that bypass other restrictions.
  3. Blacklist Pairs: Forbidden trading pairs with wildcard support.
  4. Default Policy: Fallback behavior for unconfigured routes.
1

Isolation Groups

If either asset is in an isolation group, the route is only valid if both assets are in the same isolation group.

// Example: SEED tokens can only trade with other SEED tokens
"ethereum:SEED <-> arbitrum:SEED"
2

Whitelist Overrides

Explicit exceptions that allow routes regardless of other restrictions.

// Example: Allow specific emergency routes
"bitcoin:BTC -> ethereum:WBTC"
3

Blacklist Pairs

Forbidden routes that override the default policy.

// Example: Prevent direct BTC to wrapped BTC trades
"bitcoin:BTC -> *:WBTC"
4

Default Policy

Applied when no other rules match — either “open” (allow) or “closed” (deny).

Local Route Matrix Generation

Here’s a complete script to route matrix generation for local route validation:

// Types for the policy configuration
interface RoutePolicy {
  default: 'open' | 'closed';
  isolation_groups: string[];
  blacklist_pairs: string[];
  whitelist_overrides: string[];
}

interface PolicyResponse {
  status: 'Ok' | 'Error';
  result: RoutePolicy;
  error?: string;
}

// Asset identifier type
type AssetId = string; // e.g., "ethereum:SEED", "bitcoin:BTC"

class RouteValidator {
  private policy: RoutePolicy | null = null;

  constructor(private apiBaseUrl: string, private apiKey: string) {}

  // Fetch policy from the API
  async loadPolicy(): Promise<void> {
    try {
      const response = await fetch(`${this.apiBaseUrl}/policy`, {
        headers: {
          'garden-app-id': this.apiKey,
          'accept': 'application/json'
        }
      });

      const data: PolicyResponse = await response.json();
      
      if (data.status === 'Ok') {
        this.policy = data.result;
      } else {
        throw new Error(`API Error: ${data.error}`);
      }
    } catch (error) {
      throw new Error(`Failed to load policy: ${error}`);
    }
  }

  // Check if a route is valid based on the policy
  isValidRoute(fromAsset: AssetId, toAsset: AssetId): boolean {
    if (!this.policy) {
      throw new Error('Policy not loaded. Call loadPolicy() first.');
    }

    // Can't swap to the same asset
    if (fromAsset === toAsset) {
      return false;
    }

    // Check isolation groups first (highest priority)
    if (this.isInIsolationGroup(fromAsset, toAsset)) {
      return this.isValidIsolationGroup(fromAsset, toAsset);
    }

    // Check whitelist overrides (bypass other restrictions)
    if (this.isWhitelistOverride(fromAsset, toAsset)) {
      return true;
    }

    // Check blacklist pairs
    if (this.isBlacklisted(fromAsset, toAsset)) {
      return false;
    }

    // Apply default policy
    return this.policy.default === 'open';
  }

  // Get all valid destination assets for a given source asset
  getValidDestinations(fromAsset: AssetId, allAssets: AssetId[]): AssetId[] {
    return allAssets.filter(toAsset => this.isValidRoute(fromAsset, toAsset));
  }

  // Get all possible routes from a list of assets
  getAllValidRoutes(assets: AssetId[]): Array<{ from: AssetId; to: AssetId }> {
    const routes: Array<{ from: AssetId; to: AssetId }> = [];
    
    for (const fromAsset of assets) {
      for (const toAsset of assets) {
        if (this.isValidRoute(fromAsset, toAsset)) {
          routes.push({ from: fromAsset, to: toAsset });
        }
      }
    }
    
    return routes;
  }

  // Private helper methods
  private isInIsolationGroup(fromAsset: AssetId, toAsset: AssetId): boolean {
    return this.policy!.isolation_groups.some(group => {
      const assets = this.parseIsolationGroup(group);
      return assets.includes(fromAsset) || assets.includes(toAsset);
    });
  }

  private isValidIsolationGroup(fromAsset: AssetId, toAsset: AssetId): boolean {
    return this.policy!.isolation_groups.some(group => {
      const assets = this.parseIsolationGroup(group);
      return assets.includes(fromAsset) && assets.includes(toAsset);
    });
  }

  private isWhitelistOverride(fromAsset: AssetId, toAsset: AssetId): boolean {
    return this.policy!.whitelist_overrides.some(override => 
      this.matchesPattern(fromAsset, toAsset, override)
    );
  }

  private isBlacklisted(fromAsset: AssetId, toAsset: AssetId): boolean {
    return this.policy!.blacklist_pairs.some(blacklist => 
      this.matchesPattern(fromAsset, toAsset, blacklist)
    );
  }

  private parseIsolationGroup(group: string): AssetId[] {
    // Parse "ethereum:SEED <-> arbitrum:SEED" format
    const assets = group.split('<->').map(asset => asset.trim());
    return assets;
  }

  private matchesPattern(fromAsset: AssetId, toAsset: AssetId, pattern: string): boolean {
    const [fromPattern, toPattern] = pattern.split('->').map(p => p.trim());
    
    return this.matchesAssetPattern(fromAsset, fromPattern) && 
           this.matchesAssetPattern(toAsset, toPattern);
  }

  private matchesAssetPattern(asset: AssetId, pattern: string): boolean {
    // Handle wildcard patterns
    if (pattern === '*') return true;
    
    if (pattern.includes('*')) {
      // Handle patterns like "starknet:*" or "*:USDC"
      if (pattern.endsWith(':*')) {
        const chainPattern = pattern.slice(0, -2);
        return asset.startsWith(chainPattern + ':');
      }
      if (pattern.startsWith('*:')) {
        const symbolPattern = pattern.slice(2);
        return asset.endsWith(':' + symbolPattern);
      }
    }
    
    // Exact match
    return asset === pattern;
  }
}

// Helper function to build route matrix for UI
function buildRouteMatrix(assets: AssetId[], validator: RouteValidator): Record<AssetId, AssetId[]> {
  const matrix: Record<AssetId, AssetId[]> = {};
  
  for (const fromAsset of assets) {
    matrix[fromAsset] = validator.getValidDestinations(fromAsset, assets);
  }
  
  return matrix;
}

// Export for use in your application
export { RouteValidator, buildRouteMatrix, type RoutePolicy, type AssetId };

Integration Guide

import { RouteValidator, buildRouteMatrix } from './RouteValidator';

// Initialize the RouteValidator with your API configuration.
const validator = new RouteValidator(
  'https://testnet.api.garden.finance/v2',
  'your-api-key'
);

//Fetch the policy configuration from the API.
try {
  await validator.loadPolicy();
} catch (error) {
  throw new Error('Failed to load policy:', error);
}

Use the validator to check if specific routes are allowed:

// Check individual routes.
const isValid = validator.isValidRoute('ethereum:SEED', 'arbitrum:SEED');

// Get all valid destinations for a source asset.
const destinations = validator.getValidDestinations('bitcoin:BTC', allAssets);

// Generate complete route matrix for UI.
const routeMatrix = buildRouteMatrix(allAssets, validator);

Wildcard Patterns

The system supports wildcard patterns for flexible policy configuration:

Best Practices

  1. Store the policy configuration locally and refresh it periodically rather than fetching it on every route validation.
  2. Implement proper error handling and refresh the policy when a get quote or create order fails due to an unsupported pair.