Extending Smart Accounts • Get installed plugins of a Modular Account

ERC-6900 Modular Accounts implements Plugin inspection interface IAccountLoupe.sol to support visibility in plugin configuration on-chain. This contract interface defines the method getInstalledPlugins() that clients can use to fetch the currently installed plugins on a Modular Account.

1/// @notice Get an array of all installed plugins.
2/// @return The addresses of all installed plugins.
3function getInstalledPlugins() external view returns (address[] memory);

Account Kit provides a streamlined experience of interacting with Modular Account AccoutLoupe interface easily by providing accountLoupeActions defined in @account-kit/smart-contracts package. When you connect your Modular Account to SmartAccountClient you can extend the client with accountLoupeActions, which exposes a set of methods available to call the account AccountLoupe with the client connected to the account.

Get installed plugins of a Modular Account

You should first extend the SmartAcountClient connected to a Modular Account, which has AccountLoupe implemented, with accountLoupeActions for the client to include the AccountLoupe actions.

Then, you can use the getInstalledPlugins method of the accountLoupeActions extended smart account client to get the list of installed plugin addresses for the connected Modular Account.

When using createModularAccountAlchemyClient in @account-kit/smart-contracts, the SmartAccountClient comes automatically extended with multiOwnerPluginActions, pluginManagerActions, and accountLoupeActions decorators as defaults available for use.

import { 
import modularAccountClient
modularAccountClient
} from "./client";
import {
const IPluginAbi: readonly [{ readonly type: "function"; readonly name: "onInstall"; readonly inputs: readonly [{ readonly name: "data"; readonly type: "bytes"; readonly internalType: "bytes"; }]; readonly outputs: readonly []; readonly stateMutability: "nonpayable"; }, ... 8 more ..., { ...; }]
IPluginAbi
} from "@account-kit/smart-contracts";
// returns addresses of all installed plugins const
const installedPlugins: any
installedPlugins
= await
import modularAccountClient
modularAccountClient
.
any
getInstalledPlugins
({});
if (
const installedPlugins: any
installedPlugins
.
any
length
=== 0) {
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream. * A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:


const name = 'Will Robinson'; console.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ```

Example using the `Console` class:

```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err);

myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err

const name = 'Will Robinson'; myConsole.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
console
.
Console.log(message?: any, ...optionalParams: any[]): void

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout

See util.format() for more information.

log
("account has no plugins installed.");
return; } const
const pluginAddress: any
pluginAddress
=
const installedPlugins: any
installedPlugins
[0];
// read plugin metadata of a plugin const
const metadata: any
metadata
= await
import modularAccountClient
modularAccountClient
.
any
readContract
({
address: any
address
:
const pluginAddress: any
pluginAddress
,
abi: readonly [{ readonly type: "function"; readonly name: "onInstall"; readonly inputs: readonly [{ readonly name: "data"; readonly type: "bytes"; readonly internalType: "bytes"; }]; readonly outputs: readonly []; readonly stateMutability: "nonpayable"; }, ... 8 more ..., { ...; }]
abi
:
const IPluginAbi: readonly [{ readonly type: "function"; readonly name: "onInstall"; readonly inputs: readonly [{ readonly name: "data"; readonly type: "bytes"; readonly internalType: "bytes"; }]; readonly outputs: readonly []; readonly stateMutability: "nonpayable"; }, ... 8 more ..., { ...; }]
IPluginAbi
,
functionName: string
functionName
: "pluginMetadata",
});
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream. * A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:


const name = 'Will Robinson'; console.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ```

Example using the `Console` class:

```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err);

myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err

const name = 'Will Robinson'; myConsole.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
console
.
Console.log(message?: any, ...optionalParams: any[]): void

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout

See util.format() for more information.

log
(
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (number | string)[] | null, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

stringify
(
const metadata: any
metadata
, null, 2));
// { // name: 'MultiOwnerPlugin', // version: '1.0.0', // }
import { 
class LocalAccountSigner<T extends HDAccount | PrivateKeyAccount | LocalAccount>

Represents a local account signer and provides methods to sign messages and transactions, as well as static methods to create the signer from mnemonic or private key.

LocalAccountSigner
} from "@aa-sdk/core";
import {
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.

alchemy
,
const sepolia: Chain
sepolia
} from "@account-kit/infra";
import {
function createModularAccountAlchemyClient<TSigner extends SmartAccountSigner = SmartAccountSigner<any>>(params: AlchemyModularAccountClientConfig<TSigner>): Promise<AlchemySmartAccountClient<Chain | undefined, MultiOwnerModularAccount<TSigner>, MultiOwnerPluginActions<MultiOwnerModularAccount<TSigner>> & PluginManagerActions<MultiOwnerModularAccount<TSigner>> & AccountLoupeActions<MultiOwnerModularAccount<TSigner>>>>
createModularAccountAlchemyClient
} from "@account-kit/smart-contracts";
import {
function generatePrivateKey(): Hex
generatePrivateKey
} from "viem/accounts";
export const
const chain: Chain
chain
=
const sepolia: Chain
sepolia
;
export const
const modularAccountClient: { account: MultiOwnerModularAccount<LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>>; ... 100 more ...; extend: <const client extends { ...; } & ExactPartial<...>>(fn: (client: Client<...>) => client) => Client<...>; }
modularAccountClient
= await
createModularAccountAlchemyClient<LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>>(params: AlchemyModularAccountClientConfig<...>): Promise<...>
createModularAccountAlchemyClient
({
signer: LocalAccountSigner<{ address: Address; nonceManager?: NonceManager | undefined; sign: (parameters: { hash: Hash; }) => Promise<Hex>; ... 6 more ...; type: "local"; }>
signer
:
class LocalAccountSigner<T extends HDAccount | PrivateKeyAccount | LocalAccount>

Represents a local account signer and provides methods to sign messages and transactions, as well as static methods to create the signer from mnemonic or private key.

LocalAccountSigner
.
LocalAccountSigner<T extends HDAccount | PrivateKeyAccount | LocalAccount>.privateKeyToAccountSigner(key: Hex): LocalAccountSigner<PrivateKeyAccount>

Creates a LocalAccountSigner instance using the provided private key.

privateKeyToAccountSigner
(
function generatePrivateKey(): Hex
generatePrivateKey
()),
chain: { blockExplorers?: { [key: string]: ChainBlockExplorer; default: ChainBlockExplorer; } | undefined; ... 7 more ...; testnet?: boolean | undefined; } & ChainConfig<...>

Chain for the client.

chain
,
transport: AlchemyTransport

The RPC transport

transport
:
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.

alchemy
({
apiKey: string
apiKey
: "YOUR_API_KEY" }),
});

By checking if a certain plugin address exists in the list of installed plugin addresses of a Modular Account, you can check whether a particular plugin is installed or not on a Modular Account.