npm.io
0.14.4 • Published 9 months ago

@eco-foundation/routes-sdk

Licence
MIT
Version
0.14.4
Deps
3
Size
3.3 MB
Vulns
0
Weekly
0
DeprecatedThis package is deprecated

Eco Logo

Eco - Routes SDK

Table of Contents
  1. Getting Started
  2. Refunding Expired Intents
  3. Custom Chains and Contracts (optional)
  4. Full Demo
  5. Testing
  6. Development
  7. Build

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
})

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.

See more from viem's docs

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