Advanced Relayer Example

This example details a more complex implementation of a Relayer Application. For a simple example see this example

The source for this example is available here

Setup

note: In order to run the Spy and Redis for this tutorial, you must have docker installed.

Get the code

Clone the repository, cd into the directory, and install the requirements.

git clone https://github.com/wormhole-foundation/relayer-engine.git
cd relayer-engine/examples/advanced/
npm i

Run it

Start the background services

Start the Spy to subscribe to gossiped messages on the Guardian network.

npm run testnet-spy

In another CLI window, start Redis. For this application, Redis is used as the persistence layer to keep track of VAAs we've seen.

npm run redis

Start the relayer

Once the background processes are running, we can start the relayer. This will subscribe to relevant messages from the Spy and track VAAs, taking some action when one is received.

npm run start

Code Walkthrough

Context

The first meaningful line is a Type declaration for the Context we want to provide our Relayer app.

export type MyRelayerContext = LoggingContext &
  StorageContext &
  SourceTxContext &
  TokenBridgeContext &
  StagingAreaContext &
  WalletContext;

This type, which we later use to parameterize the generic RelayerApp, specifies the union of Context objects that are available to the RelayerApp.

Because the Context object is passed to the callback for processors, providing a type parameterized type definition ensures the appropriate fields are available within the callback on the Context object.

App Creation

Next we instantiate a RelayerApp, passing our Context type to parameterize it.

const app = new RelayerApp<MyRelayerContext>(Environment.TESTNET);

For this example we've defined a class, ApiController, to provide methods we'll pass to our processor callbacks. Note that the ctx argument type matches the Context type we defined.

export class ApiController {
  processFundsTransfer = async (ctx: MyRelayerContext, next: Next) => {
    // ...
  };
}

This is not required but a pattern like this helps organize the codebase as the RelayerApp grows.

We instantiate our controller class and begin to configure our application by passing the Spy URL, storage layer, and logger.

const fundsCtrl = new ApiController();
const namespace = "simple-relayer-example";
const store = new RedisStorage({
  attempts: 3,
  namespace, // used for redis key namespace
  queueName: "relays",
});

// ...

app.spy("localhost:7073");
app.useStorage(store);
app.logger(rootLogger);

Middleware

With our app configured, we can begin to add Middleware.

Middleware is the term for the functional components we wish to apply to each VAA received.

The RelayerApp defines a use method which accepts one or more Middleware instances, also parameterized with a Context type.

use(...middleware: Middleware<ContextT>[] | ErrorMiddleware<ContextT>[])

By passing the use method an instance of some Middleware, we add it to the pipeline of handlers invoked by the RelayerApp.

Note that the order the Middleware is added here matters since a VAA is passed through each in the same order.

// we want an instance of a logger available on the context
app.use(logging(rootLogger));
// we want to check for any missed VAAs if we receive out of order sequence ids
app.use(missedVaas(app, { namespace: "simple", logger: rootLogger }));
// we want to apply the chain specific providers to the context passed downstream
app.use(providers());
// enrich the context with details about the token bridge
app.use(tokenBridgeContracts());
// ensure we use redis safely in a concurrent environment
app.use(stagingArea());
// make sure we have the source tx hash
app.use(sourceTx());

Subscriptions

With our Middleware setup, we can configure a subscription to receive only the VAAs we care about.

Here we set up a subscription request to receive VAAs that originated from Solana and were emitted by the address DZnkkTmCiFWfYTfT41X3Rd1kDgozqzxWaHqsw6W4x2oe.

On receipt of a VAA that matches this filter, the fundsCtrl.processFundsTransfer callback is invoked with an instance of the Context object that has already been passed through the Middleware we set up before.

app
  .chain(CHAIN_ID_SOLANA)
  .address(
    "DZnkkTmCiFWfYTfT41X3Rd1kDgozqzxWaHqsw6W4x2oe",
    fundsCtrl.processFundsTransfer,
  );

To subscribe to more chains or addresses, this pattern can be repeated or the multiple method can be called with an object of ChainId to Address

app.multiple(
  {
    [CHAIN_ID_SOLANA]: "DZnkkTmCiFWfYTfT41X3Rd1kDgozqzxWaHqsw6W4x2oe"
    [CHAIN_ID_ETH]: ["0xabc1230000000...","0xdef456000....."]
  },
  fundsCtrl.processFundsTransfer,
);

Error Handling

The last Middleware we apply is an error handler, which will be called any time an upstream Middleware component throws an error.

Note that there are 3 arguments to this function which hints to the RelayerApp that it should be used to process errors.

app.use(async (err, ctx, next) => {
  ctx.logger.error("error middleware triggered");
});

Start listening

Finally, calling app.listen() will start the RelayerApp, issuing subscription requests and handling VAAs as we've configured it.

The listen method is async and await-ing it will block until the program dies from an unrecoverable error, the process is killed, or the app is stopped with app.stop().

Bonus UI

For this example, we've provided a default UI using koa.

When the program is running, you may open a browser to http://localhost:3000/ui to see details about the running application.

Going further

The included default functionality may be insufficient for your use case.

If you'd like to apply some specific intermediate processing steps, consider implementing some custom Middleware. Be sure to include the appropriate Context in the RelayerApp type parameterization for any fields you wish to have added to the Context object passed to downstream Middleware.

If you'd prefer a storage layer besides redis, simply implement the storage interface.

Wormhole integration complete?

Let us know so we can list your project in our ecosystem directory and introduce you to our global, multichain community!

Reach out now!

Last updated