Features

Umi Plugins

Whilst Umi is a small zero-dependency framework, it is designed to be extended with plugins. Plugins allow us to not only interact with its interfaces or swap out its interface implementations but also to add new features to Umi itself.

Using plugins

To install a Umi plugin, you may simply call the use method on the Umi instance. This use method returns the Umi instance so they can be chained together.

import { somePlugin } from 'some-umi-library';
import { myLocalPlugin } from '../plugins';

umi.use(somePlugin).use(myLocalPlugin);

It is worth noting that libraries will often provide a function that returns a plugin instead of the plugin itself. This is done so that we can pass any arguments to configure the behaviour of the plugin.

import { somePlugin } from 'some-umi-library';
import { myLocalPlugin } from '../plugins';

umi.use(somePlugin(somePluginOptions))
  .use(myLocalPlugin(myLocalPluginOptions));

To stay consistent, the plugins provided by Umi always follow this pattern even if they don't require any arguments. Here are some examples:

import { web3JsRpc } from '@metaplex-foundation/umi-rpc-web3js';
import { mockStorage } from '@metaplex-foundation/umi-storage-mock';
import { httpDownloader } from '@metaplex-foundation/umi-downloader-http';

umi.use(web3JsRpc('https://api.mainnet-beta.solana.com'))
  .use(mockStorage())
  .use(httpDownloader());

Creating plugins

Under the hood, Umi defines a plugin as an object with an install function that can be used to extend the Umi instance however we want.

export const myPlugin: UmiPlugin = {
  install(umi: Umi) {
    // Do something with the Umi instance.
  },
}

As mentioned above, it is recommended to export plugin functions so we can request any argument that might be needed from the end user.

export const myPlugin = (myPluginOptions?: MyPluginOptions): UmiPlugin => ({
  install(umi: Umi) {
    // Do something with the Umi instance.
  },
})

What to do in a plugin

Now that we know how to create a plugin, let's have a look at some examples of what we can do with them.

Setting interface implementations

One of the most common use cases for plugins is to assign an implementation to one or several Umi interfaces. Here's an example of setting a fictional MyRpc implementation to the rpc interface. Notice how we can pass the Umi instance to the MyRpc implementation so it can rely on other interfaces if needed.

export const myRpc = (endpoint: string): UmiPlugin => ({
  install(umi: Umi) {
    umi.rpc = new MyRpc(umi, endpoint);
  },
})

Decorating interface implementations

Another way of setting interface implementations is to decorate existing ones. This allows the end-user to compose plugins together by adding extra functionality to existing implementations without worrying about their underlying implementation details.

Here's an example of a plugin that decorates the rpc interface such that it logs all sent transactions to a third-party service.

export const myLoggingRpc = (provider: LoggingProvider): UmiPlugin => ({
  install(umi: Umi) {
    umi.rpc = new MyLoggingRpc(umi.rpc, provider);
  },
})

Creating bundles

Since plugins can also call the use method on the Umi instance, it is possible to install plugins within plugins. This allows us to create bundles of plugins that can be installed together.

For instance, this is how Umi's "defaults" plugin bundle is defined:

export const defaultPlugins = (
  endpoint: string,
  rpcOptions?: Web3JsRpcOptions
): UmiPlugin => ({
  install(umi) {
    umi.use(dataViewSerializer());
    umi.use(defaultProgramRepository());
    umi.use(fetchHttp());
    umi.use(httpDownloader());
    umi.use(web3JsEddsa());
    umi.use(web3JsRpc(endpoint, rpcOptions));
    umi.use(web3JsTransactionFactory());
  },
});

Using interfaces

On top of setting and updating Umi's interfaces, plugins can also use them. One common use case for this is to allow libraries to register new programs to the program repository interfaces. Here's an example illustrating how the Token Metadata library registers its program. Notice how it sets the override argument to false so that the program is only registered if it doesn't already exist.

export const mplTokenMetadata = (): UmiPlugin => ({
  install(umi) {
    umi.programs.add(createMplTokenMetadataProgram(), false);
  },
});

Extending the Umi instance

Last but not least, plugins can also extend the feature set of the Umi instance. This allows libraries to provide their own interfaces, extend existing ones, etc.

A good example of that is the Candy Machine library which stores all candy guards in a repository — much like the program repository. This allows end-users to register their own guards so they can be recognised when creating, fetching and minting from candy machines with associated candy guards. To make this work, the library adds a new guards property to the Umi instance and assigns a new guard repository to it.

export const mplCandyMachine = (): UmiPlugin => ({
  install(umi) {
    umi.guards = new DefaultGuardRepository(umi);
    umi.guards.add(botTaxGuardManifest);
    umi.guards.add(solPaymentGuardManifest);
    umi.guards.add(tokenPaymentGuardManifest);
    // ...
  },
});

The slight issue with the code above is that the Umi type no longer reflects the actual instance. That is, TypeScript will complain that the guards property doesn't exist on the Umi type. To fix this, we can use TypeScript's Module Augmentation to extend the Umi type so it includes the new property like so

declare module '@metaplex-foundation/umi' {
  interface Umi {
    guards: GuardRepository;
  }
}

This module augmentation can also be used to extend an existing interface. For instance, we could assign a new RPC interface that contains additional methods whilst letting TypeScript know about our added methods like so.

export const myRpcWithAddedMethods = (): UmiPlugin => ({
  install(umi) {
    umi.rpc = new MyRpcWithAddedMethods(umi.rpc);
  },
});

declare module '@metaplex-foundation/umi' {
  interface Umi {
    rpc: MyRpcWithAddedMethods;
  }
}
Previous
Kinobi