Table of Contents
Getting Started
Installing
Install the package and the @eco-foundation/routes-ts peer dependency to your project:
npm install @eco-foundation/routes-sdk @eco-foundation/routes-ts
Eco Routes peer dependency
The Eco team is constantly improving our protocol. Sometimes, this involves making upgrades to our contracts, which will generate new contract addresses that we will publish in our routes-ts package. When this happens, be aware that if your application is still running with an outdated version of the routes-ts package, some intents might not get fulfilled.
Upgrading to the latest protocol contracts is easy! Simply run:
npm install @eco-foundation/routes-ts@latest
To install the latest contracts, and the SDK will automatically use them.
Note: Upgrading the routes-ts package by a minor or major version might require upgrading the SDK as well, run:
npm install @eco-foundation/routes-sdk@latest
To install the latest version of the SDK.
Quick Start
Create a simple intent
To create a simple stable send intent, create an instance of the RoutesService and call createSimpleIntent with the required parameters:
import { RoutesService } from '@eco-foundation/routes-sdk';
const address = '0x1234567890123456789012345678901234567890';
const originChainID = 10;
const spendingToken = RoutesService.getStableAddress(originChainID, 'USDC');
const spendingTokenLimit = BigInt(10000000); // 10 USDC
const destinationChainID = 8453;
const receivingToken = RoutesService.getStableAddress(destinationChainID, 'USDC');
const amount = BigInt(1000000); // 1 USDC
const routesService = new RoutesService();
// create a simple stable transfer from my wallet on the origin chain to my wallet on the destination chain
const intent = routesService.createSimpleIntent({
creator: address,
originChainID,
spendingToken,
spendingTokenLimit,
destinationChainID,
receivingToken,
amount,
recipient: address, // optional, defaults to the creator if not passed
})
Create a native send intent
To create a native token (ETH, MATIC, etc.) send intent, use the createNativeSendIntent method:
import { RoutesService } from '@eco-foundation/routes-sdk';
const address = '0x1234567890123456789012345678901234567890';
const originChainID = 10; // Optimism
const destinationChainID = 8453; // Base
const amount = BigInt("10000000000000000"); // 0.01 ETH (in wei)
const limit = BigInt("1000000000000000000"); // 1 ETH
const routesService = new RoutesService();
// create a native token send from my wallet on the origin chain to my wallet on the destination chain
const intent = routesService.createNativeSendIntent({
creator: address,
originChainID,
destinationChainID,
amount,
limit,
recipient: address, // optional, defaults to the creator if not passed
})
Request quotes for an intent and select a quote (recommended)
To request quotes for an intent and select the cheapest quote, use the OpenQuotingClient and selectCheapestQuote functions.
Then, you can apply the quote by calling applyQuoteToIntent on the RoutesService instance:
import { OpenQuotingClient, selectCheapestQuote, selectCheapestQuoteNativeSend } from '@eco-foundation/routes-sdk';
const openQuotingClient = new OpenQuotingClient({ dAppID: 'my-dapp' });
try {
const quotes = await openQuotingClient.requestQuotesForIntent(intent);
// select quote
const selectedQuote = selectCheapestQuote(quotes);
// OR, for native send intents
const selectedQuote = selectCheapestQuoteNativeSend(quotes);
// apply quote to intent
const intentWithQuote = routesService.applyQuoteToIntent({ intent, quote: selectedQuote });
}
catch (error) {
console.error('Quotes not available', error);
}
Custom selectors (optional)
Depending on your use case, you might want to select some quote based on some other criteria, not just the cheapest. You can create a custom selector function to do this.
import { SolverQuote } from '@eco-foundation/routes-sdk';
// custom selector fn using SolverQuote type
export function selectMostExpensiveQuote(quotes: SolverQuote[]): SolverQuote {
return quotes.reduce((mostExpensive, quote) => {
const mostExpensiveSum = mostExpensive ? sum(mostExpensive.quoteData.tokens.map(({ balance }) => balance)) : BigInt(-1);
const quoteSum = sum(quote.quoteData.tokens.map(({ balance }) => balance));
return quoteSum > mostExpensiveSum ? quote : mostExpensive;
});
}
Implications of not requesting a quote
If you do not request a quote for your intent and you continue with publishing it, you risk the possibility of your intent not being fulfilled by any solvers (because of an insufficient token limit) or losing any surplus amount from your spendingTokenLimit that the solver didn't need to fulfill your intent. This is why requesting a quote is strongly recommended.
Publishing the intent
The SDK gives you what you need so that you can publish the intent to the origin chain with whatever web3 library you choose, here is an example of how to publish our quoted intent using viem!
import { createWalletClient, privateKeyToAccount, webSocket, http, erc20Abi, parseEventLogs } from 'viem';
import { optimism } from 'viem/chains';
import { IntentSourceAbi } from '@eco-foundation/routes-ts';
const account = privateKeyToAccount('YOUR PRIVATE KEY HERE')
const originChain = optimism;
const rpcUrl = 'YOUR RPC URL'
const originWalletClient = createWalletClient({
account,
transport: webSocket(rpcUrl) // OR http(rpcUrl)
})
const originPublicClient = createPublicClient({
chain: originChain,
transport: webSocket(rpcUrl) // OR http(rpcUrl)
})
const intentSourceContract = routesService.getProtocolContractAddress(originChain.id, 'IntentSource');
try {
// approve the quoted amount to account for fees
await Promise.all(intentWithQuote.reward.tokens.map(async ({ token, amount }) => {
const approveTxHash = await originWalletClient.writeContract({
abi: erc20Abi,
address: token,
functionName: 'approve',
args: [intentSourceContract, amount],
chain: originChain,
account
})
await originPublicClient.waitForTransactionReceipt({ hash: approveTxHash })
}))
const publishTxHash = await originWalletClient.writeContract({
abi: IntentSourceAbi,
address: intentSourceContract,
functionName: 'publishAndFund',
args: [intentWithQuote, false],
chain: originChain,
account,
value: intentWithQuote.reward.nativeValue // Send the required native value if applicable
})
const publishReceipt = await originPublicClient.waitForTransactionReceipt({ hash: publishTxHash });
const logs = parseEventLogs({
abi: IntentSourceAbi,
logs: publishReceipt.logs
})
const intentCreatedEvent = logs.find((log) => log.eventName === 'IntentCreated')
if (!intentCreatedEvent) {
throw new Error('IntentCreated event not found in logs')
}
const intentHash = intentCreatedEvent.args.hash;
}
catch (error) {
console.error('Intent creation failed', error)
}
Tracking the intent fulfillment
Once the intent is published, you can track its fulfillment by listening to the Fulfillment event emitted by the Inbox contract.
import { createPublicClient, webSocket, Hex } from 'viem';
import { base } from 'viem/chains';
import { InboxAbi } from '@eco-foundation/routes-ts';
const inboxContract = routesService.getProtocolContractAddress(originChain.id, 'Inbox');
const rpcUrl = 'YOUR RPC URL';
const destinationPublicClient = createPublicClient({
chain: base,
transport: webSocket(rpcUrl)
});
try {
// get block to start watching from
const latestBlock = await destinationPublicClient.getBlockNumber();
const fulfillmentTxHash = await new Promise<Hex>((resolve, reject) => {
const unwatch = destinationPublicClient.watchContractEvent({
fromBlock: latestBlock - BigInt(25),
abi: InboxAbi,
address: inboxContract,
eventName: 'Fulfillment',
args: {
_hash: intentHash
}
onLogs(logs) {
if (logs.length > 0) {
const txHash = logs[0]!.transactionHash;
unwatch();
resolve(txHash);
}
},
onError(error) {
unwatch();
reject(error);
}
});
});
console.log('Fulfillment transaction hash:', fulfillmentTxHash);
} catch (error) {
console.error('Error tracking intent fulfillment:', error);
}
Once the fulfillment transaction is mined, you can then check the balance of the receiving token in your wallet on the destination chain to confirm that the funds have been received.
Refunding Expired Intents
If an intent expires before it's fulfilled by a solver, you can refund the tokens you deposited when creating the intent. To do this, you'll need the original intent data, which you can retrieve from the IntentCreated event log that was emitted when you published the intent.
Parsing Intent from Event Log
When you publish an intent, the transaction receipt will contain an IntentCreated event. You can parse this event to get the intent data needed for refunding:
import { parseEventLogs } from 'viem';
import { IntentSourceAbi } from '@eco-foundation/routes-ts';
import { RoutesService } from '@eco-foundation/routes-sdk';
// After publishing intent, get the transaction receipt
const receipt = await publicClient.waitForTransactionReceipt({ hash: publishTxHash });
// Parse the logs to find the IntentCreated event
const logs = parseEventLogs({
abi: IntentSourceAbi,
logs: receipt.logs
});
const intentCreatedEvent = logs.find((log) => log.eventName === 'IntentCreated');
// Parse the intent from the event arguments
const parsedIntent = RoutesService.parseIntentFromIntentCreatedEventArgs(intentCreatedEvent!.args);
Executing the Refund
Once you have the parsed intent and it has expired, you can call the refund function on the IntentSource contract:
import { IntentSourceAbi } from '@eco-foundation/routes-ts';
const intentSourceContract = routesService.getProtocolContractAddress(originChain.id, 'IntentSource');
// Make sure the intent has expired before attempting refund
const currentTime = new Date();
if (currentTime > parsedIntent.expiryTime) {
const refundTxHash = await walletClient.writeContract({
abi: IntentSourceAbi,
address: intentSourceContract,
functionName: 'refund',
args: [parsedIntent],
chain: originChain,
account
});
await publicClient.waitForTransactionReceipt({ hash: refundTxHash });
console.log('Refund successful!');
} else {
console.log('Intent has not expired yet');
}
Custom Chains and Contracts (optional)
The SDK is designed to work with the @eco-foundation/routes-ts package, which provides the default chains and contracts. However, you can pass custom chains and contracts to the SDK if needed.
To do this, you can create a custom RoutesService instance with your own chains and contracts:
import { RoutesService, ProtocolAddresses } from '@eco-foundation/routes-sdk';
const customProtocolAddresses: ProtocolAddresses = {
123456789: {
IntentSource: '0x1234567890123456789012345678901234567890',
MetaProver: '0x0987654321098765432109876543210987654321',
},
"123456789-pre": { // preprod contracts
IntentSource: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
MetaProver: '0x1234567890123456789012345678901234567890',
}
}
const routesService = new RoutesService({
customProtocolAddresses,
})
Note: Custom contract addresses passed on already-supported chains will override any default addresses from @eco-foundation/routes-ts.
Full Demo
For a full example of creating an intent and tracking it until it's fulfilled, see the Eco Routes SDK Demo.
Testing
First, create a .env file in the sdk directory and populate it with all the environment variables listed in .env.example. Then to run tests, run:
npm run test
Development
Run development mode:
npm run dev
Build
Run build:
npm run build