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
Copy
Ask AI
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
Name | Type | Required | Description |
---|---|---|---|
route.salt | bytes32 | Y | Unique identifier for the route |
route.source | uint256 | Y | Source chain ID |
route.destination | uint256 | Y | Destination chain ID |
route.inbox | address | Y | Inbox contract on destination |
route.tokens | Token[] | Y | Tokens involved in the intent |
route.calls | Call[] | N | Contract calls to execute |
reward.creator | address | Y | Address creating the intent |
reward.prover | address | Y | Prover contract address |
reward.deadline | uint256 | Y | Unix timestamp when intent expires |
reward.nativeValue | uint256 | Y | Native token reward (0 if none) |
reward.tokens | Token[] | Y | ERC20 reward tokens |
Example Code
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
POST https://api.eco.com/v1/quotes
Request Parameters
Name | Type | Required | Description |
---|---|---|---|
intent | Intent | Y | The intent object |
originChain | uint256 | Y | Chain ID where intent is published |
Example Code
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
function publishAndFund(Intent calldata intent, bool usePermit2) external payable
Parameters
Name | Type | Required | Description |
---|---|---|---|
intent | Intent | Y | Complete intent structure |
usePermit2 | bool | Y | Use Permit2 for approvals (usually false) |
Example Code
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Network | Chain ID | IntentSource | Inbox | Prover |
---|---|---|---|---|
Optimism | 10 | TBD | TBD | TBD |
Base | 8453 | TBD | TBD | TBD |
Arbitrum | 42161 | TBD | TBD | TBD |
Error Codes
Code | Description | Resolution |
---|---|---|
IntentAlreadyExists | Intent with same hash exists | Use different salt/nonce |
IntentNotExpired | Cannot refund before deadline | Wait for deadline to pass |
IntentNotClaimed | Intent already proven | Check prover contract |
NO_QUOTES | No solvers available | Retry or adjust parameters |