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
dockerinstalled.
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 iRun it
Start the background services
Start the Spy to subscribe to gossiped messages on the Guardian network.
npm run testnet-spyIn 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 redisStart 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 startCode 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
Middlewareis 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!
Last updated
Was this helpful?

