Namespace Rules
This guide explains how to use username Namespace Rules and how to implement custom ones.
Namespace Rules allow administrators to add requirements or constraints that will be applied when a Username in a given Namespace is created or assigned to an Account.
Lens provides three built-in Group rules:
UsernamePricePerLengthNamespaceRule - Requires an ERC-20 payment to create a Username.
TokenGatedNamespaceRule - Requires an account to hold a certain token to create a Username.
UsernameLengthNamespaceRule - Restricts the length of Usernames.
For the UsernamePricePerLengthNamespaceRule, a 1.5% Lens treasury fee is deducted from the payment before the remaining amount is transferred to the designated recipient.
To keep usernames web-friendly across the ecosystem, the Namespace primitive enforces a maximum length of 255 characters.
Two additional built-in rules are also applied by default to every new Namespace:
UsernameReservedNamespaceRule - This rule allows the Namespace owner or admins to reserve a specific set of usernames. See Reserved Usernames for more information.
UsernameSimpleCharsetNamespaceRule - This rule limits valid characters to a-z, 0-9, -, and _, ensuring consistency. Usernames cannot begin with - or _.
Using Namespace Rules
As part of creating Custom Namespaces, you can pass a rules object that defines the required rules and/or an anyOf set, where satisfying any one rule allows the Username creation or assignment to succeed. These rules can be built-in or custom.
This section presumes you are familiar with the process of creating a Namespace on Lens.
Username Price per Length Namespace Rule
This rule can be applied to a Username namespace to require an ERC-20 payment based on the length of the Username being created.
- TypeScript
- GraphQL
- React
UsernamePricePerLengthNamespaceRule
import { bigDecimal, evmAddress } from "@lens-protocol/client";import { createUsernameNamespace } from "@lens-protocol/client/actions";
const result = await createUsernameNamespace(sessionClient, { symbol: "FOO", namespace: "foo", rules: { required: [ { usernamePricePerLengthRule: { cost: { currency: evmAddress("0x5678…"), value: bigDecimal('0.5'), // Token value in its main unit }, recipient: evmAddress("0x1234…"), costOverrides: [ { amount: bigDecimal('5'), length: 1, }, { amount: bigDecimal('4'), length: 2, }, { amount: bigDecimal('3'), length: 3, }, { amount: bigDecimal('2'), length: 4, } ] } } ],});
Token Gated Namespace Rule
This rule requires holding a certain balance of a token (fungible or non-fungible) to create a Username.
Configuration includes the token address, the token standard (ERC-20, ERC-721, or ERC-1155), and the required token amount. For ERC-1155 tokens, an additional token type ID is required.
- TypeScript
- GraphQL
- React
Username Length Namespace Rule
This rule can restricts the minimum and/or maximum length of Usernames.
- TypeScript
- GraphQL
- React
UsernameLengthNamespaceRule
import { bigDecimal, evmAddress } from "@lens-protocol/client";import { createUsernameNamespace } from "@lens-protocol/client/actions";
const result = await createUsernameNamespace(sessionClient, { symbol: "FOO", namespace: "foo", rules: { required: [ { usernameLengthRule: { minLength: 3, maxLength: 10, }, }, ], },});
Custom Namespace Rule
You can also use custom rules by specifying the rule contract address, when it applies, and the configuration parameters as key-value pairs.
- TypeScript
- GraphQL
- React
Custom Group Rule
import { blockchainData, evmAddress, NamespaceRuleExecuteOn,} from "@lens-protocol/client";import { createUsernameNamespace } from "@lens-protocol/client/actions";
const result = await createUsernameNamespace(sessionClient, { symbol: "FOO", namespace: "foo", rules: { required: [ { unknownRule: { address: evmAddress("0x1234…"), executeOn: [ NamespaceRuleExecuteOn.Creating, NamespaceRuleExecuteOn.Assigning, ], params: [ { raw: { // 32 bytes key (e.g., keccak(name)) key: blockchainData("0xac5f04…"), // an ABI encoded value value: blockchainData("0x00"), }, }, ], }, }, ], },});
Update a Namespace Rules
To update a Namespace rules configuration, follow these steps.
You MUST be authenticated as Builder and be either the owner or an admin of the Namespace you intend to configure.
First, inspect the namespace.rules field to know the current rules configuration.
- TypeScript
- GraphQL
Keep note of the Rule IDs you might want to remove.
Next, update the rules configuration of the Namespace as follows.
- TypeScript
- GraphQL
- React
Use the updateNamespaceRules action to update the rules configuration of a given namespace.
- TypeScript
- GraphQL
- React
Then, handle the result using the adapter for the library of your choice:
See the Transaction Lifecycle guide for more information on how to determine the status of the transaction.
Building a Namespace Rule
Let's illustrate the process with an example. We will build a custom Namespace Rule that requires Usernames to be created only if their length has an specific parity, for example, all usernames must have an even length.
To build a custom Namespace Rule, you must implement the following INamespaceRule interface:
INamespaceRule.sol
import {KeyValue} from "contracts/core/types/Types.sol";
interface INamespaceRule { function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external;
function processCreation( bytes32 configSalt, address originalMsgSender, address account, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external;
function processRemoval( bytes32 configSalt, address originalMsgSender, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external;
function processAssigning( bytes32 configSalt, address originalMsgSender, address account, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external;
function processUnassigning( bytes32 configSalt, address originalMsgSender, address account, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external;}
Each function of this interface must assume to be invoked by the Namespace contract. In other words, assume the msg.sender will be the Namespace contract.
A Lens dependency package with all relevant interfaces will be available soon.
First, implement the configure function. This function has the purpose of initializing any required state for the rule to work properly.
It receives two parameters, a 32-byte configuration salt (configSalt), and an array of custom parameters as key-value pairs (ruleParams).
The configSalt is there to allow the same rule contract to be used many times, with different configurations, for the same Namespace. So, for a given Namespace Rule implementation, the pair (Namespace Address, Configuration Salt) should identify a rule configuration.
The configure function can be called multiple times by the same Namespace passing the same configuration salt in order to update that rule configuration (i.e. reconfigure it).
The ruleParams is an array of key-value pairs that can be used to pass any custom configuration parameters to the rule. Each key is bytes32, we put the hash of the parameter name there, and each value is bytes, we set the ABI-encoded parameter value there. Given that ruleParams is an array, this allows the rule to define which parameters are optional and which are required, acting accordingly when any of them are not present.
In our example, we only need to decode a boolean parameter, which will indicate if the rule will enforce Usernames to have an even or an odd length. Let's define a storage mapping to store this configuration:
contract UsernameParityLengthNamespaceRule is INamespaceRule { mapping(address namespace => mapping(bytes32 configSalt => bool mustBeEven)) internal _mustBeEvenLength;}
The configuration is stored in the mapping using the Namespace contract address (msg.sender) and the configuration salt as keys. With this setup, the same rule can be used by different Namespaces, as well as be used by the same Namespace many times.
Now let's code the configure function itself, decoding the boolean parameter and storing it in the mapping:
contract UsernameParityLengthNamespaceRule is INamespaceRule { mapping(address namespace => mapping(bytes32 configSalt => bool mustBeEven)) internal _mustBeEvenLength;
function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external override { bool mustBeEven = true; // We set `true` as default value for (uint256 i = 0; i < ruleParams.length; i++) { if (ruleParams[i].key == keccak256("lens.param.mustBeEven")) { mustBeEven = abi.decode(ruleParams[i].value, (bool)); break; } } _mustBeEvenLength[msg.sender][configSalt] = mustBeEven; }}
We treated the mustBeEven parameter as optional, defaulting to true (even length) when not present.
Next, implement the processCreation function. This function is invoked by the Namespace contract every time a username is being created, so then our custom logic can be applied to shape under which conditions this operation can succeed.
The function receives the configuration salt (configSalt), the address that is trying to create the Username (originalMsgSender), the account who will own the created Username, the username being created, an array of key-value pairs with the custom parameters passed to the Namespace (primitiveParams), and an array of key-value pairs in case the rule requires additional parameters to work (ruleParams).
The function must revert if the requirements imposed by the rule are not met.
contract UsernameParityLengthNamespaceRule is INamespaceRule { mapping(address namespace => mapping(bytes32 configSalt => bool mustBeEven)) internal _mustBeEvenLength;
// ...
function processCreation( bytes32 configSalt, address originalMsgSender, address account, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external view override { // Retrieve the rule configuration bool mustBeEven = _mustBeEvenLength[msg.sender][configSalt];
// Get the length of the username being created uint256 usernameLength = bytes(username).length;
// Check if the length is even (otherwise it is odd) bool isEvenLength = usernameLength % 2 == 0;
// Require the parity of the username being created to match // the parity required by the rule require(isEvenLength == mustBeEven); }
// ...}
Next, implement the processRemoval function. This function is invoked by the Namespace contract every time a username is being removed.
The function receives the configuration salt (configSalt), the address that is trying to remove the Username (originalMsgSender), the username being removed, an array of key-value pairs with the custom parameters passed to the Namespace (primitiveParams), and an array of key-value pairs in case the rule requires additional parameters to work (ruleParams).
The function must revert if the requirements imposed by the rule are not met. In our example, the parity rule does not apply to removal, so we revert with NotImplemented. This is good practice in case the rule is accidentally enabled for this selector.
contract UsernameParityLengthNamespaceRule is INamespaceRule {
// ...
function processRemoval( bytes32 configSalt, address originalMsgSender, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external pure override { revert Errors.NotImplemented(); }
// ...}
Next, implement the processAssigning function. This function is invoked by the Namespace contract every time a username is being assigned to an account.
The function receives the configuration salt (configSalt), the address that is trying to assign the Username (originalMsgSender), the account who the username will be assigned to, the username being assigned, an array of key-value pairs with the custom parameters passed to the Namespace (primitiveParams), and an array of key-value pairs in case the rule requires additional parameters to work (ruleParams).
The function must revert if the requirements imposed by the rule are not met. Similar to removal, our parity rule does not apply to the assignment operation, so we revert with NotImplemented.
contract UsernameParityLengthNamespaceRule is INamespaceRule {
// ...
function processAssigning( bytes32 configSalt, address originalMsgSender, address account, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external pure override { revert Errors.NotImplemented(); }
// ...}
Finally, implement the processUnassigning function. This function is invoked by the Namespace contract every time a username is being unassigned from an account.
The function receives the configuration salt (configSalt), the address that is trying to unassign the Username (originalMsgSender), the account who the username will be unassigned from, the username being unassigned, an array of key-value pairs with the custom parameters passed to the Namespace (primitiveParams), and an array of key-value pairs in case the rule requires additional parameters to work (ruleParams).
The function must revert if the requirements imposed by the rule are not met. Again, our parity rule does not apply to the unassigning process, so we revert with NotImplemented.
contract UsernameParityLengthNamespaceRule is INamespaceRule {
// ...
function processUnassigning( bytes32 configSalt, address originalMsgSender, address account, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external pure override { revert Errors.NotImplemented(); }}
Now the UsernameParityLengthNamespaceRule is ready to be applied to any Namespace. See the full code below:
contract UsernameParityLengthNamespaceRule is INamespaceRule {
mapping(address => mapping(bytes32 => bool)) internal _mustBeEvenLength;
function configure(bytes32 configSalt, KeyValue[] calldata ruleParams) external override { bool mustBeEven = true; // We set `true` as default value for (uint256 i = 0; i < ruleParams.length; i++) { if (ruleParams[i].key == keccak256("lens.param.mustBeEven")) { mustBeEven = abi.decode(ruleParams[i].value, (bool)); break; } } _mustBeEvenLength[msg.sender][configSalt] = mustBeEven; }
function processCreation( bytes32 configSalt, address originalMsgSender, address account, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external view override { // Retrieve the rule configuration bool mustBeEven = _mustBeEvenLength[msg.sender][configSalt];
// Get the length of the username being created uint256 usernameLength = bytes(username).length;
// Check if the length is even (otherwise it is odd) bool isEvenLength = usernameLength % 2 == 0;
// Require the parity of the username being created to match // the parity required by the rule require(isEvenLength == mustBeEven); }
function processRemoval( bytes32 configSalt, address originalMsgSender, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external pure override { revert Errors.NotImplemented(); }
function processAssigning( bytes32 configSalt, address originalMsgSender, address account, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external pure override { revert Errors.NotImplemented(); }
function processUnassigning( bytes32 configSalt, address originalMsgSender, address account, string calldata username, KeyValue[] calldata primitiveParams, KeyValue[] calldata ruleParams ) external pure override { revert Errors.NotImplemented(); }}
Stay tuned for API integration of rules and more guides!