Eco Routes Direct API Integration

Overview

This guide demonstrates how to create and execute cross-chain intents using direct smart contract interactions with the Eco Routes Protocol, based on the routes-cli implementation.

Intent Creation Flow

1. Build Intent Structure

Create an intent with the required parameters for cross-chain execution.

Intent Interface

interface Route {
  salt: string;
  source: number;
  destination: number;
  inbox: string;
  tokens: Token[];
  calls: Call[];
}

interface Reward {
  creator: string;
  prover: string;
  deadline: number;
  nativeValue: string;
  tokens: RewardToken[];
}

interface Intent {
  route: Route;
  reward: Reward;
}

Parameters

NameTypeRequiredDescription
route.saltbytes32YUnique identifier for the route
route.sourceuint256YSource chain ID
route.destinationuint256YDestination chain ID
route.inboxaddressYInbox contract on destination
route.tokensToken[]YTokens involved in the intent
route.callsCall[]NContract calls to execute
reward.creatoraddressYAddress creating the intent
reward.proveraddressYProver contract address
reward.deadlineuint256YUnix timestamp when intent expires
reward.nativeValueuint256YNative token reward (0 if none)
reward.tokensToken[]YERC20 reward tokens

Example Code

import { ethers } from 'ethers';

// Generate unique salt
const salt = ethers.utils.keccak256(
  ethers.utils.defaultAbiCoder.encode(
    ['address', 'uint256'],
    [userAddress, Date.now()]
  )
);

// Build intent structure
const intent = {
  route: {
    salt: salt,
    source: 10, // Optimism
    destination: 8453, // Base
    inbox: "0x...", // Inbox contract on Base
    tokens: [
      {
        token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
        amount: "1000000" // 1 USDC
      }
    ],
    calls: [] // Empty for simple transfer
  },
  reward: {
    creator: userAddress,
    prover: "0x...", // Prover contract address
    deadline: Math.floor(Date.now() / 1000) + 3600, // 1 hour
    nativeValue: "0",
    tokens: [] // Will be populated from quote
  }
};

2. Calculate Intent Hash

Generate the unique identifier for tracking the intent.

Hash Calculation

function getIntentHash(intent: Intent): string {
  const routeHash = ethers.utils.keccak256(
    ethers.utils.defaultAbiCoder.encode(
      [
        'bytes32', // salt
        'uint256', // source
        'uint256', // destination
        'address', // inbox
        'tuple(address token, uint256 amount)[]', // tokens
        'tuple(address target, bytes callData, uint256 value)[]' // calls
      ],
      [
        intent.route.salt,
        intent.route.source,
        intent.route.destination,
        intent.route.inbox,
        intent.route.tokens,
        intent.route.calls
      ]
    )
  );

  const rewardHash = ethers.utils.keccak256(
    ethers.utils.defaultAbiCoder.encode(
      [
        'address', // creator
        'address', // prover
        'uint256', // deadline
        'uint256', // nativeValue
        'tuple(address token, uint256 amount)[]' // tokens
      ],
      [
        intent.reward.creator,
        intent.reward.prover,
        intent.reward.deadline,
        intent.reward.nativeValue,
        intent.reward.tokens
      ]
    )
  );

  return ethers.utils.keccak256(
    ethers.utils.solidityPack(
      ['bytes32', 'bytes32'],
      [routeHash, rewardHash]
    )
  );
}

3. Request Quotes

Get pricing from solvers via the Open Quoting Client.

API Endpoint

POST https://api.eco.com/v1/quotes

Request Parameters

NameTypeRequiredDescription
intentIntentYThe intent object
originChainuint256YChain ID where intent is published

Example Code

async function requestQuotes(intent: Intent): Promise<Quote[]> {
  const response = await fetch('https://api.eco.com/v1/quotes', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-App-ID': 'routes-cli'
    },
    body: JSON.stringify({
      intent: intent,
      originChain: intent.route.source
    })
  });

  const data = await response.json();
  
  if (!data.quotes || data.quotes.length === 0) {
    throw new Error('No quotes available');
  }

  // Apply best quote to intent
  const bestQuote = data.quotes[0];
  intent.reward.tokens = bestQuote.rewardTokens.map((t: any) => ({
    token: t.token,
    amount: t.amount
  }));

  return data.quotes;
}

Publishing Flow

4. Approve Reward Tokens

Grant the IntentSource contract permission to spend reward tokens.

ERC20 Approval

const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet(PRIVATE_KEY, provider);

const ERC20_ABI = [
  'function approve(address spender, uint256 amount) returns (bool)',
  'function allowance(address owner, address spender) view returns (uint256)'
];

async function approveTokens(intent: Intent, intentSourceAddress: string) {
  for (const reward of intent.reward.tokens) {
    const token = new ethers.Contract(reward.token, ERC20_ABI, signer);
    
    // Check current allowance
    const allowance = await token.allowance(
      signer.address,
      intentSourceAddress
    );
    
    if (allowance.lt(reward.amount)) {
      console.log(`Approving ${reward.token}...`);
      const tx = await token.approve(intentSourceAddress, reward.amount);
      await tx.wait();
    }
  }
}

5. Publish and Fund Intent

Submit the intent to the IntentSource contract in a single transaction.

Contract Function

function publishAndFund(Intent calldata intent, bool usePermit2) external payable

Parameters

NameTypeRequiredDescription
intentIntentYComplete intent structure
usePermit2boolYUse Permit2 for approvals (usually false)

Example Code

const INTENT_SOURCE_ABI = [
  {
    "inputs": [
      {
        "components": [
          {
            "components": [
              { "name": "salt", "type": "bytes32" },
              { "name": "source", "type": "uint256" },
              { "name": "destination", "type": "uint256" },
              { "name": "inbox", "type": "address" },
              {
                "components": [
                  { "name": "token", "type": "address" },
                  { "name": "amount", "type": "uint256" }
                ],
                "name": "tokens",
                "type": "tuple[]"
              },
              {
                "components": [
                  { "name": "target", "type": "address" },
                  { "name": "callData", "type": "bytes" },
                  { "name": "value", "type": "uint256" }
                ],
                "name": "calls",
                "type": "tuple[]"
              }
            ],
            "name": "route",
            "type": "tuple"
          },
          {
            "components": [
              { "name": "creator", "type": "address" },
              { "name": "prover", "type": "address" },
              { "name": "deadline", "type": "uint256" },
              { "name": "nativeValue", "type": "uint256" },
              {
                "components": [
                  { "name": "token", "type": "address" },
                  { "name": "amount", "type": "uint256" }
                ],
                "name": "tokens",
                "type": "tuple[]"
              }
            ],
            "name": "reward",
            "type": "tuple"
          }
        ],
        "name": "intent",
        "type": "tuple"
      },
      { "name": "usePermit2", "type": "bool" }
    ],
    "name": "publishAndFund",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "anonymous": false,
    "inputs": [
      { "indexed": true, "name": "hash", "type": "bytes32" },
      { "indexed": false, "name": "salt", "type": "bytes32" },
      { "indexed": false, "name": "source", "type": "uint256" },
      { "indexed": false, "name": "destination", "type": "uint256" },
      { "indexed": false, "name": "inbox", "type": "address" },
      { "indexed": false, "name": "tokens", "type": "tuple[]" },
      { "indexed": false, "name": "calls", "type": "tuple[]" },
      { "indexed": false, "name": "creator", "type": "address" },
      { "indexed": false, "name": "prover", "type": "address" },
      { "indexed": false, "name": "deadline", "type": "uint256" },
      { "indexed": false, "name": "nativeValue", "type": "uint256" },
      { "indexed": false, "name": "rewardTokens", "type": "tuple[]" }
    ],
    "name": "IntentCreated",
    "type": "event"
  }
];

async function publishIntent(intent: Intent, intentSourceAddress: string) {
  const intentSource = new ethers.Contract(
    intentSourceAddress,
    INTENT_SOURCE_ABI,
    signer
  );

  // Calculate gas estimate
  const gasEstimate = await intentSource.estimateGas.publishAndFund(
    intent,
    false, // usePermit2
    { value: intent.reward.nativeValue }
  );

  // Publish with 20% gas buffer
  const tx = await intentSource.publishAndFund(
    intent,
    false,
    {
      value: intent.reward.nativeValue,
      gasLimit: gasEstimate.mul(120).div(100)
    }
  );

  console.log(`Publishing intent: ${tx.hash}`);
  const receipt = await tx.wait();

  // Extract intent hash from event
  const event = receipt.events?.find(e => e.event === 'IntentCreated');
  const intentHash = event?.args?.hash;

  console.log(`Intent published with hash: ${intentHash}`);
  return { txHash: tx.hash, intentHash };
}

Monitoring Flow

6. Track Fulfillment

Monitor the Inbox contract on the destination chain.

Event Monitoring

const INBOX_ABI = [
  {
    "anonymous": false,
    "inputs": [
      { "indexed": true, "name": "intentHash", "type": "bytes32" },
      { "indexed": true, "name": "solver", "type": "address" }
    ],
    "name": "Fulfillment",
    "type": "event"
  }
];

async function trackFulfillment(
  intentHash: string,
  inboxAddress: string,
  destRpcUrl: string
): Promise<FulfillmentResult> {
  const destProvider = new ethers.providers.JsonRpcProvider(destRpcUrl);
  const inbox = new ethers.Contract(inboxAddress, INBOX_ABI, destProvider);

  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      reject(new Error('Fulfillment timeout'));
    }, 300000); // 5 minutes

    // Listen for fulfillment event
    inbox.on('Fulfillment', (hash, solver) => {
      if (hash === intentHash) {
        clearTimeout(timeout);
        console.log(`Intent fulfilled by solver: ${solver}`);
        resolve({ intentHash: hash, solver });
      }
    });
  });
}

Refund Flow

7. Claim Refund (If Expired)

Reclaim reward tokens if the intent expires without fulfillment.

Refund Function

const REFUND_ABI = [
  {
    "inputs": [
      { "name": "routeHash", "type": "bytes32" },
      {
        "components": [
          { "name": "creator", "type": "address" },
          { "name": "prover", "type": "address" },
          { "name": "deadline", "type": "uint256" },
          { "name": "nativeValue", "type": "uint256" },
          {
            "components": [
              { "name": "token", "type": "address" },
              { "name": "amount", "type": "uint256" }
            ],
            "name": "tokens",
            "type": "tuple[]"
          }
        ],
        "name": "reward",
        "type": "tuple"
      }
    ],
    "name": "refund",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
];

async function refundIntent(
  routeHash: string,
  reward: Reward,
  intentSourceAddress: string
) {
  const intentSource = new ethers.Contract(
    intentSourceAddress,
    REFUND_ABI,
    signer
  );

  const tx = await intentSource.refund(routeHash, reward);
  await tx.wait();
  
  console.log('Refund completed');
}

Complete Implementation

import { ethers } from 'ethers';
import dotenv from 'dotenv';

dotenv.config();

// Contract addresses (replace with actual addresses)
const ADDRESSES = {
  optimism: {
    chainId: 10,
    intentSource: "0x...",
    prover: "0x...",
    usdc: "0x7F5c764cBc14f9669B88837ca1490cCa17c31607"
  },
  base: {
    chainId: 8453,
    inbox: "0x...",
    usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
  }
};

async function main() {
  // Setup providers and signer
  const originProvider = new ethers.providers.JsonRpcProvider(process.env.OPTIMISM_RPC);
  const destProvider = new ethers.providers.JsonRpcProvider(process.env.BASE_RPC);
  const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, originProvider);

  const amount = "1000000"; // 1 USDC

  // 1. Create intent
  const salt = ethers.utils.keccak256(
    ethers.utils.defaultAbiCoder.encode(
      ['address', 'uint256'],
      [signer.address, Date.now()]
    )
  );

  const intent = {
    route: {
      salt: salt,
      source: ADDRESSES.optimism.chainId,
      destination: ADDRESSES.base.chainId,
      inbox: ADDRESSES.base.inbox,
      tokens: [
        {
          token: ADDRESSES.base.usdc,
          amount: amount
        }
      ],
      calls: []
    },
    reward: {
      creator: signer.address,
      prover: ADDRESSES.optimism.prover,
      deadline: Math.floor(Date.now() / 1000) + 3600,
      nativeValue: "0",
      tokens: [] as any[]
    }
  };

  // 2. Get quotes
  console.log('Requesting quotes...');
  const response = await fetch('https://api.eco.com/v1/quotes', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-App-ID': 'routes-cli'
    },
    body: JSON.stringify({
      intent,
      originChain: intent.route.source
    })
  });

  const data = await response.json();
  if (!data.quotes?.length) throw new Error('No quotes available');
  
  intent.reward.tokens = data.quotes[0].rewardTokens;

  // 3. Calculate intent hash
  const intentHash = getIntentHash(intent);
  console.log(`Intent hash: ${intentHash}`);

  // 4. Approve tokens
  console.log('Approving tokens...');
  const ERC20_ABI = [
    'function approve(address spender, uint256 amount) returns (bool)'
  ];

  for (const reward of intent.reward.tokens) {
    const token = new ethers.Contract(reward.token, ERC20_ABI, signer);
    const tx = await token.approve(ADDRESSES.optimism.intentSource, reward.amount);
    await tx.wait();
  }

  // 5. Publish intent
  console.log('Publishing intent...');
  const INTENT_SOURCE_ABI = [
    // Include full ABI from above
  ];

  const intentSource = new ethers.Contract(
    ADDRESSES.optimism.intentSource,
    INTENT_SOURCE_ABI,
    signer
  );

  const publishTx = await intentSource.publishAndFund(intent, false, {
    value: intent.reward.nativeValue
  });
  const receipt = await publishTx.wait();

  console.log(`Published in tx: ${receipt.transactionHash}`);

  // 6. Monitor fulfillment
  console.log('Waiting for fulfillment...');
  const INBOX_ABI = [
    {
      "anonymous": false,
      "inputs": [
        { "indexed": true, "name": "intentHash", "type": "bytes32" },
        { "indexed": true, "name": "solver", "type": "address" }
      ],
      "name": "Fulfillment",
      "type": "event"
    }
  ];

  const inbox = new ethers.Contract(ADDRESSES.base.inbox, INBOX_ABI, destProvider);

  await new Promise((resolve, reject) => {
    const timeout = setTimeout(() => reject(new Error('Timeout')), 300000);

    inbox.on('Fulfillment', (hash, solver) => {
      if (hash === intentHash) {
        clearTimeout(timeout);
        console.log(`✅ Fulfilled by ${solver}`);
        resolve({ hash, solver });
      }
    });
  });
}

function getIntentHash(intent: any): string {
  const routeHash = ethers.utils.keccak256(
    ethers.utils.defaultAbiCoder.encode(
      ['bytes32', 'uint256', 'uint256', 'address', 'tuple(address,uint256)[]', 'tuple(address,bytes,uint256)[]'],
      [intent.route.salt, intent.route.source, intent.route.destination, intent.route.inbox, intent.route.tokens, intent.route.calls]
    )
  );

  const rewardHash = ethers.utils.keccak256(
    ethers.utils.defaultAbiCoder.encode(
      ['address', 'address', 'uint256', 'uint256', 'tuple(address,uint256)[]'],
      [intent.reward.creator, intent.reward.prover, intent.reward.deadline, intent.reward.nativeValue, intent.reward.tokens]
    )
  );

  return ethers.utils.keccak256(
    ethers.utils.solidityPack(['bytes32', 'bytes32'], [routeHash, rewardHash])
  );
}

main().catch(console.error);

Contract Addresses

Mainnet

NetworkChain IDIntentSourceInboxProver
Optimism10TBDTBDTBD
Base8453TBDTBDTBD
Arbitrum42161TBDTBDTBD

Error Codes

CodeDescriptionResolution
IntentAlreadyExistsIntent with same hash existsUse different salt/nonce
IntentNotExpiredCannot refund before deadlineWait for deadline to pass
IntentNotClaimedIntent already provenCheck prover contract
NO_QUOTESNo solvers availableRetry or adjust parameters