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:- Isolation Groups (Highest Priority): Assets that can ONLY trade with each other.
- Whitelist Overrides: Explicit exceptions that bypass other restrictions.
- Blacklist Pairs: Forbidden trading pairs with wildcard support.
- 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 };
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use reqwest;
use anyhow::{Result, Error};
// Types for the policy configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutePolicy {
pub default: String, // "open" or "closed"
pub isolation_groups: Vec<String>,
pub blacklist_pairs: Vec<String>,
pub whitelist_overrides: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PolicyResponse {
pub status: String,
pub result: RoutePolicy,
pub error: Option<String>,
}
pub type AssetId = String;
pub struct RouteValidator {
api_base_url: String,
api_key: String,
policy: Option<RoutePolicy>,
}
impl RouteValidator {
pub fn new(api_base_url: String, api_key: String) -> Self {
RouteValidator {
api_base_url,
api_key,
policy: None,
}
}
// Fetch policy from the API
pub async fn load_policy(&mut self) -> Result<()> {
let client = reqwest::Client::new();
let url = format!("{}/policy", self.api_base_url);
let response = client
.get(&url)
.header("garden-app-id", &self.api_key)
.header("accept", "application/json")
.send()
.await?;
let data: PolicyResponse = response.json().await?;
if data.status == "Ok" {
self.policy = Some(data.result);
Ok(())
} else {
Err(Error::msg(format!("API Error: {:?}", data.error)))
}
}
// Check if a route is valid based on the policy
pub fn is_valid_route(&self, from_asset: &AssetId, to_asset: &AssetId) -> Result<bool> {
let policy = self.policy.as_ref()
.ok_or_else(|| Error::msg("Policy not loaded. Call load_policy() first."))?;
// Can't swap to the same asset
if from_asset == to_asset {
return Ok(false);
}
// Check isolation groups first (highest priority)
if self.is_in_isolation_group(from_asset, to_asset, policy) {
return Ok(self.is_valid_isolation_group(from_asset, to_asset, policy));
}
// Check whitelist overrides (bypass other restrictions)
if self.is_whitelist_override(from_asset, to_asset, policy) {
return Ok(true);
}
// Check blacklist pairs
if self.is_blacklisted(from_asset, to_asset, policy) {
return Ok(false);
}
// Apply default policy
Ok(policy.default == "open")
}
// Get all valid destination assets for a given source asset
pub fn get_valid_destinations(&self, from_asset: &AssetId, all_assets: &[AssetId]) -> Result<Vec<AssetId>> {
let mut valid_destinations = Vec::new();
for to_asset in all_assets {
if self.is_valid_route(from_asset, to_asset)? {
valid_destinations.push(to_asset.clone());
}
}
Ok(valid_destinations)
}
// Get all possible routes from a list of assets
pub fn get_all_valid_routes(&self, assets: &[AssetId]) -> Result<Vec<(AssetId, AssetId)>> {
let mut routes = Vec::new();
for from_asset in assets {
for to_asset in assets {
if self.is_valid_route(from_asset, to_asset)? {
routes.push((from_asset.clone(), to_asset.clone()));
}
}
}
Ok(routes)
}
// Private helper methods
fn is_in_isolation_group(&self, from_asset: &AssetId, to_asset: &AssetId, policy: &RoutePolicy) -> bool {
policy.isolation_groups.iter().any(|group| {
let assets = self.parse_isolation_group(group);
assets.contains(from_asset) || assets.contains(to_asset)
})
}
fn is_valid_isolation_group(&self, from_asset: &AssetId, to_asset: &AssetId, policy: &RoutePolicy) -> bool {
policy.isolation_groups.iter().any(|group| {
let assets = self.parse_isolation_group(group);
assets.contains(from_asset) && assets.contains(to_asset)
})
}
fn is_whitelist_override(&self, from_asset: &AssetId, to_asset: &AssetId, policy: &RoutePolicy) -> bool {
policy.whitelist_overrides.iter().any(|override_pattern| {
self.matches_pattern(from_asset, to_asset, override_pattern)
})
}
fn is_blacklisted(&self, from_asset: &AssetId, to_asset: &AssetId, policy: &RoutePolicy) -> bool {
policy.blacklist_pairs.iter().any(|blacklist_pattern| {
self.matches_pattern(from_asset, to_asset, blacklist_pattern)
})
}
fn parse_isolation_group(&self, group: &str) -> Vec<AssetId> {
group.split("<->")
.map(|asset| asset.trim().to_string())
.collect()
}
fn matches_pattern(&self, from_asset: &AssetId, to_asset: &AssetId, pattern: &str) -> bool {
let parts: Vec<&str> = pattern.split("->").map(|p| p.trim()).collect();
if parts.len() != 2 {
return false;
}
self.matches_asset_pattern(from_asset, parts[0]) &&
self.matches_asset_pattern(to_asset, parts[1])
}
fn matches_asset_pattern(&self, asset: &AssetId, pattern: &str) -> bool {
// Handle wildcard patterns
if pattern == "*" {
return true;
}
if pattern.contains('*') {
// Handle patterns like "starknet:*" or "*:USDC"
if pattern.ends_with(":*") {
let chain_pattern = &pattern[..pattern.len() - 2];
return asset.starts_with(&format!("{}:", chain_pattern));
}
if pattern.starts_with("*:") {
let symbol_pattern = &pattern[2..];
return asset.ends_with(&format!(":{}", symbol_pattern));
}
}
// Exact match
asset == pattern
}
}
// Helper function to build route matrix for UI
pub fn build_route_matrix(assets: &[AssetId], validator: &RouteValidator) -> Result<HashMap<AssetId, Vec<AssetId>>> {
let mut matrix = HashMap::new();
for from_asset in assets {
let destinations = validator.get_valid_destinations(from_asset, assets)?;
matrix.insert(from_asset.clone(), destinations);
}
Ok(matrix)
}
package main
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"errors"
)
// Types for the policy configuration
type RoutePolicy struct {
Default string `json:"default"`
IsolationGroups []string `json:"isolation_groups"`
BlacklistPairs []string `json:"blacklist_pairs"`
WhitelistOverrides []string `json:"whitelist_overrides"`
}
type PolicyResponse struct {
Status string `json:"status"`
Result RoutePolicy `json:"result"`
Error *string `json:"error,omitempty"`
}
type AssetId string
type RouteValidator struct {
apiBaseURL string
apiKey string
policy *RoutePolicy
}
func NewRouteValidator(apiBaseURL, apiKey string) *RouteValidator {
return &RouteValidator{
apiBaseURL: apiBaseURL,
apiKey: apiKey,
policy: nil,
}
}
// Fetch policy from the API
func (rv *RouteValidator) LoadPolicy() error {
client := &http.Client{}
url := fmt.Sprintf("%s/policy", rv.apiBaseURL)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
req.Header.Set("garden-app-id", rv.apiKey)
req.Header.Set("accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var data PolicyResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return err
}
if data.Status == "Ok" {
rv.policy = &data.Result
return nil
}
errorMsg := "Unknown error"
if data.Error != nil {
errorMsg = *data.Error
}
return fmt.Errorf("API Error: %s", errorMsg)
}
// Check if a route is valid based on the policy
func (rv *RouteValidator) IsValidRoute(fromAsset, toAsset AssetId) (bool, error) {
if rv.policy == nil {
return false, errors.New("policy not loaded. Call LoadPolicy() first")
}
// Can't swap to the same asset
if fromAsset == toAsset {
return false, nil
}
// Check isolation groups first (highest priority)
if rv.isInIsolationGroup(fromAsset, toAsset) {
return rv.isValidIsolationGroup(fromAsset, toAsset), nil
}
// Check whitelist overrides (bypass other restrictions)
if rv.isWhitelistOverride(fromAsset, toAsset) {
return true, nil
}
// Check blacklist pairs
if rv.isBlacklisted(fromAsset, toAsset) {
return false, nil
}
// Apply default policy
return rv.policy.Default == "open", nil
}
// Get all valid destination assets for a given source asset
func (rv *RouteValidator) GetValidDestinations(fromAsset AssetId, allAssets []AssetId) ([]AssetId, error) {
var validDestinations []AssetId
for _, toAsset := range allAssets {
if valid, err := rv.IsValidRoute(fromAsset, toAsset); err != nil {
return nil, err
} else if valid {
validDestinations = append(validDestinations, toAsset)
}
}
return validDestinations, nil
}
// Get all possible routes from a list of assets
func (rv *RouteValidator) GetAllValidRoutes(assets []AssetId) ([]struct{From, To AssetId}, error) {
var routes []struct{From, To AssetId}
for _, fromAsset := range assets {
for _, toAsset := range assets {
if valid, err := rv.IsValidRoute(fromAsset, toAsset); err != nil {
return nil, err
} else if valid {
routes = append(routes, struct{From, To AssetId}{From: fromAsset, To: toAsset})
}
}
}
return routes, nil
}
// Private helper methods
func (rv *RouteValidator) isInIsolationGroup(fromAsset, toAsset AssetId) bool {
for _, group := range rv.policy.IsolationGroups {
assets := rv.parseIsolationGroup(group)
if rv.contains(assets, fromAsset) || rv.contains(assets, toAsset) {
return true
}
}
return false
}
func (rv *RouteValidator) isValidIsolationGroup(fromAsset, toAsset AssetId) bool {
for _, group := range rv.policy.IsolationGroups {
assets := rv.parseIsolationGroup(group)
if rv.contains(assets, fromAsset) && rv.contains(assets, toAsset) {
return true
}
}
return false
}
func (rv *RouteValidator) isWhitelistOverride(fromAsset, toAsset AssetId) bool {
for _, override := range rv.policy.WhitelistOverrides {
if rv.matchesPattern(fromAsset, toAsset, override) {
return true
}
}
return false
}
func (rv *RouteValidator) isBlacklisted(fromAsset, toAsset AssetId) bool {
for _, blacklist := range rv.policy.BlacklistPairs {
if rv.matchesPattern(fromAsset, toAsset, blacklist) {
return true
}
}
return false
}
func (rv *RouteValidator) parseIsolationGroup(group string) []AssetId {
parts := strings.Split(group, "<->")
var assets []AssetId
for _, part := range parts {
assets = append(assets, AssetId(strings.TrimSpace(part)))
}
return assets
}
func (rv *RouteValidator) matchesPattern(fromAsset, toAsset AssetId, pattern string) bool {
parts := strings.Split(pattern, "->")
if len(parts) != 2 {
return false
}
fromPattern := strings.TrimSpace(parts[0])
toPattern := strings.TrimSpace(parts[1])
return rv.matchesAssetPattern(fromAsset, fromPattern) &&
rv.matchesAssetPattern(toAsset, toPattern)
}
func (rv *RouteValidator) matchesAssetPattern(asset AssetId, pattern string) bool {
// Handle wildcard patterns
if pattern == "*" {
return true
}
if strings.Contains(pattern, "*") {
// Handle patterns like "starknet:*" or "*:USDC"
if strings.HasSuffix(pattern, ":*") {
chainPattern := pattern[:len(pattern)-2]
return strings.HasPrefix(string(asset), chainPattern+":")
}
if strings.HasPrefix(pattern, "*:") {
symbolPattern := pattern[2:]
return strings.HasSuffix(string(asset), ":"+symbolPattern)
}
}
// Exact match
return string(asset) == pattern
}
func (rv *RouteValidator) contains(slice []AssetId, item AssetId) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
// Helper function to build route matrix for UI
func BuildRouteMatrix(assets []AssetId, validator *RouteValidator) (map[AssetId][]AssetId, error) {
matrix := make(map[AssetId][]AssetId)
for _, fromAsset := range assets {
destinations, err := validator.GetValidDestinations(fromAsset, assets)
if err != nil {
return nil, err
}
matrix[fromAsset] = destinations
}
return matrix, nil
}
import requests
from typing import List, Dict, Optional, Tuple
from dataclasses import dataclass
import json
# Types for the policy configuration
@dataclass
class RoutePolicy:
default: str # "open" or "closed"
isolation_groups: List[str]
blacklist_pairs: List[str]
whitelist_overrides: List[str]
@dataclass
class PolicyResponse:
status: str
result: RoutePolicy
error: Optional[str] = None
AssetId = str
class RouteValidator:
def __init__(self, api_base_url: str, api_key: str):
self.api_base_url = api_base_url
self.api_key = api_key
self.policy: Optional[RoutePolicy] = None
async def load_policy(self) -> None:
"""Fetch policy from the API"""
try:
headers = {
'garden-app-id': self.api_key,
'accept': 'application/json'
}
response = requests.get(f"{self.api_base_url}/policy", headers=headers)
response.raise_for_status()
data = response.json()
if data['status'] == 'Ok':
result = data['result']
self.policy = RoutePolicy(
default=result['default'],
isolation_groups=result['isolation_groups'],
blacklist_pairs=result['blacklist_pairs'],
whitelist_overrides=result['whitelist_overrides']
)
else:
raise Exception(f"API Error: {data.get('error', 'Unknown error')}")
except Exception as e:
raise Exception(f"Failed to load policy: {e}")
def is_valid_route(self, from_asset: AssetId, to_asset: AssetId) -> bool:
"""Check if a route is valid based on the policy"""
if self.policy is None:
raise Exception("Policy not loaded. Call load_policy() first.")
# Can't swap to the same asset
if from_asset == to_asset:
return False
# Check isolation groups first (highest priority)
if self._is_in_isolation_group(from_asset, to_asset):
return self._is_valid_isolation_group(from_asset, to_asset)
# Check whitelist overrides (bypass other restrictions)
if self._is_whitelist_override(from_asset, to_asset):
return True
# Check blacklist pairs
if self._is_blacklisted(from_asset, to_asset):
return False
# Apply default policy
return self.policy.default == 'open'
def get_valid_destinations(self, from_asset: AssetId, all_assets: List[AssetId]) -> List[AssetId]:
"""Get all valid destination assets for a given source asset"""
return [to_asset for to_asset in all_assets if self.is_valid_route(from_asset, to_asset)]
def get_all_valid_routes(self, assets: List[AssetId]) -> List[Tuple[AssetId, AssetId]]:
"""Get all possible routes from a list of assets"""
routes = []
for from_asset in assets:
for to_asset in assets:
if self.is_valid_route(from_asset, to_asset):
routes.append((from_asset, to_asset))
return routes
# Private helper methods
def _is_in_isolation_group(self, from_asset: AssetId, to_asset: AssetId) -> bool:
for group in self.policy.isolation_groups:
assets = self._parse_isolation_group(group)
if from_asset in assets or to_asset in assets:
return True
return False
def _is_valid_isolation_group(self, from_asset: AssetId, to_asset: AssetId) -> bool:
for group in self.policy.isolation_groups:
assets = self._parse_isolation_group(group)
if from_asset in assets and to_asset in assets:
return True
return False
def _is_whitelist_override(self, from_asset: AssetId, to_asset: AssetId) -> bool:
return any(self._matches_pattern(from_asset, to_asset, override)
for override in self.policy.whitelist_overrides)
def _is_blacklisted(self, from_asset: AssetId, to_asset: AssetId) -> bool:
return any(self._matches_pattern(from_asset, to_asset, blacklist)
for blacklist in self.policy.blacklist_pairs)
def _parse_isolation_group(self, group: str) -> List[AssetId]:
"""Parse 'ethereum:SEED <-> arbitrum:SEED' format"""
return [asset.strip() for asset in group.split('<->')]
def _matches_pattern(self, from_asset: AssetId, to_asset: AssetId, pattern: str) -> bool:
parts = [p.strip() for p in pattern.split('->')]
if len(parts) != 2:
return False
from_pattern, to_pattern = parts
return (self._matches_asset_pattern(from_asset, from_pattern) and
self._matches_asset_pattern(to_asset, to_pattern))
def _matches_asset_pattern(self, asset: AssetId, pattern: str) -> bool:
"""Handle wildcard patterns"""
if pattern == '*':
return True
if '*' in pattern:
# Handle patterns like "starknet:*" or "*:USDC"
if pattern.endswith(':*'):
chain_pattern = pattern[:-2]
return asset.startswith(chain_pattern + ':')
if pattern.startswith('*:'):
symbol_pattern = pattern[2:]
return asset.endswith(':' + symbol_pattern)
# Exact match
return asset == pattern
# Helper function to build route matrix for UI
def build_route_matrix(assets: List[AssetId], validator: RouteValidator) -> Dict[AssetId, List[AssetId]]:
"""Build route matrix for UI components"""
matrix = {}
for from_asset in assets:
matrix[from_asset] = validator.get_valid_destinations(from_asset, assets)
return matrix
# Usage example
async def example():
validator = RouteValidator('https://testnet.api.garden.finance/v2', 'your-api-key')
try:
# Load policy from API
await validator.load_policy()
# Example assets
assets = [
'ethereum:SEED',
'arbitrum:SEED',
'bitcoin:BTC',
'ethereum:WBTC',
'starknet:WBTC'
]
# Check specific routes
print('ethereum:SEED -> arbitrum:SEED:', validator.is_valid_route('ethereum:SEED', 'arbitrum:SEED'))
print('ethereum:SEED -> bitcoin:BTC:', validator.is_valid_route('ethereum:SEED', 'bitcoin:BTC'))
print('bitcoin:BTC -> starknet:WBTC:', validator.is_valid_route('bitcoin:BTC', 'starknet:WBTC'))
# Get all valid destinations from ethereum:SEED
valid_destinations = validator.get_valid_destinations('ethereum:SEED', assets)
print('Valid destinations from ethereum:SEED:', valid_destinations)
# Get all valid routes
all_routes = validator.get_all_valid_routes(assets)
print('All valid routes:', all_routes)
except Exception as error:
print('Error:', error)
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);
}
// 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:Chain-level wildcards
Chain-level wildcards
Match all assets on a specific chain:
// Block all routes from Bitcoin to any Starknet asset.
"bitcoin:BTC -> starknet:*"
// Allow any Ethereum asset to trade with Bitcoin.
"ethereum:* -> bitcoin:BTC"
Asset-level wildcards
Asset-level wildcards
Match specific assets across all chains:
// Block all WBTC variants from trading with native BTC.
"*:WBTC -> bitcoin:BTC"
// Allow all USDC variants to trade with each other.
"*:USDC -> *:USDC"
Universal wildcards
Universal wildcards
Match any asset (use with caution):
// Allow any asset to trade with Ethereum WETH.
"* -> ethereum:WETH"
Best practices
- Store the policy configuration locally and refresh it periodically rather than fetching it on every route validation.
- Implement proper error handling and refresh the policy when a get quote or create order fails due to an unsupported pair.