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.
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 modularAccountClientmodularAccountClient } 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: anyinstalledPlugins = await import modularAccountClientmodularAccountClient.anygetInstalledPlugins({});
if (const installedPlugins: anyinstalledPlugins.anylength === 0) {
var console: ConsoleThe 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[]): voidPrints 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: anypluginAddress = const installedPlugins: anyinstalledPlugins[0];
// read plugin metadata of a plugin
const const metadata: anymetadata = await import modularAccountClientmodularAccountClient.anyreadContract({
address: anyaddress: const pluginAddress: anypluginAddress,
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: stringfunctionName: "pluginMetadata",
});
var console: ConsoleThe 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[]): voidPrints 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: JSONAn 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: anymetadata, 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): AlchemyTransportCreates 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: Chainsepolia } 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(): HexgeneratePrivateKey } from "viem/accounts";
export const const chain: Chainchain = const sepolia: Chainsepolia;
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(): HexgeneratePrivateKey()),
chain: {
blockExplorers?: {
[key: string]: ChainBlockExplorer;
default: ChainBlockExplorer;
} | undefined;
... 7 more ...;
testnet?: boolean | undefined;
} & ChainConfig<...>Chain for the client.
chain,
transport: AlchemyTransportThe RPC transport
transport: function alchemy(config: AlchemyTransportConfig): AlchemyTransportCreates 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: stringapiKey: "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.