The RuntimeRBAC class provides type-safe access to Bloxchain RuntimeRBAC contracts with dynamic role-based access control and flexible permission management.
RuntimeRBAC extends BaseStateMachine with advanced role management:
- Dynamic role creation and management via batch configuration
- Flexible permission system with function-level access control
- Meta-transaction support for role operations
- Event-driven role updates for external monitoring
Note: Function schema registration has been moved to GuardController for better architectural separation. RuntimeRBAC focuses on role and permission management, while GuardController handles execution control and function schema management.
import { RuntimeRBAC } from '@bloxchain/sdk/typescript'
import { createPublicClient, createWalletClient, http } from 'viem'
import { mainnet } from 'viem/chains'
// Initialize clients
const publicClient = createPublicClient({
chain: mainnet,
transport: http()
})
const walletClient = createWalletClient({
account: privateKeyToAccount('0x...'),
chain: mainnet,
transport: http()
})
// Create RuntimeRBAC instance
const runtimeRBAC = new RuntimeRBAC(
publicClient,
walletClient,
'0x...', // contract address
mainnet
)const role = await runtimeRBAC.getRole('0x...') // role hash
console.log('Role info:', {
name: role.roleName,
hash: role.roleHash,
maxWallets: role.maxWallets,
walletCount: role.walletCount,
isProtected: role.isProtected
})const hasRole = await runtimeRBAC.hasRole(
'0x...', // role hash
'0x...' // account address
)
console.log('Account has role:', hasRole)const wallets = await runtimeRBAC.getAuthorizedWallets('0x...') // role hash
console.log('Authorized wallets in role:', wallets)const roles = await runtimeRBAC.getWalletRoles('0x...') // wallet address
console.log('Roles for wallet:', roles)
// Returns array of role hashes assigned to the wallet
// Uses reverse index for efficient O(n) lookup where n = wallet's role countconst roles = await runtimeRBAC.getSupportedRoles()
console.log('Supported roles:', roles)const schema = await runtimeRBAC.getFunctionSchema('0xa9059cbb') // function selector
console.log('Function schema:', {
signature: schema.functionSignature,
selector: schema.functionSelector,
operationType: schema.operationType,
operationName: schema.operationName,
supportedActionsBitmap: schema.supportedActionsBitmap,
isProtected: schema.isProtected,
handlerForSelectors: schema.handlerForSelectors
})
// Supported actions as array: EngineBlox.convertBitmapToActions(schema.supportedActionsBitmap)const functions = await runtimeRBAC.getSupportedFunctions()
console.log('Supported functions:', functions)const permissions = await runtimeRBAC.getActiveRolePermissions('0x...') // role hash
permissions.forEach(permission => {
console.log('Permission:', {
functionSelector: permission.functionSelector,
grantedActionsBitmap: permission.grantedActionsBitmap,
handlerForSelectors: permission.handlerForSelectors
})
})const hasPermission = await runtimeRBAC.hasActionPermission(
'0x...', // account address
'0xa9059cbb', // function selector
TxAction.EXECUTE_TIME_DELAY_REQUEST
)
console.log('Has action permission:', hasPermission)Role permissions store a handlerForSelectors array on each FunctionPermission. On-chain behavior is split as follows (see contracts/core/lib/EngineBlox.sol):
-
Grant time (
addFunctionToRole):_validateHandlerForSelectorschecks that every entry in the permission’shandlerForSelectorsis allowed by the function schema for thatfunctionSelectorwhen the schema hasenforceHandlerRelations(strict mode). This does not re-run on everyhasActionPermissionread. -
Runtime permission (
hasActionPermission/roleHasActionPermission): Only whether the wallet’s roles include thefunctionSelectorand theTxActionbitmap. The storedhandlerForSelectorslist on the role is not consulted again on each call. -
Meta / dual-selector paths (
_validateExecutionAndHandlerPermissions): RequireshasActionPermissionfor bothexecutionSelectorandhandlerSelector. If the handler function schema hasenforceHandlerRelations, the engine also requiresexecutionSelectorto appear infunctions[handlerSelector].handlerForSelectors— a global handler→execution graph on the schema, independent of which role row granted access. -
Flexible schemas: If
enforceHandlerRelationsis false for a schema, that global pairing check is skipped by design. The@custom:security OPERATIONAL MODESdiscussion (strict vs flexible handler wiring) lives onEngineBlox.registerFunctionincontracts/core/lib/EngineBlox.sol—that is the schema-registration entry point (this repository does not expose a separateregisterFunctionSchemasymbol). When wiring roles, whitelists, and meta-tx flows in production, follow the action ordering and batch constraints incontracts/core/access/lib/definitions/RuntimeRBACDefinitions.sol(role-config / meta) andcontracts/core/execution/lib/definitions/GuardControllerDefinitions.sol(guard-config / macros) so schemas, grants, and calls stay consistent.
RuntimeRBAC uses batch configuration for all role and function management operations. This allows multiple changes to be applied atomically via meta-transactions.
The batch system supports the following action types:
enum RoleConfigActionType {
CREATE_ROLE,
REMOVE_ROLE,
ADD_WALLET,
REVOKE_WALLET,
ADD_FUNCTION_TO_ROLE,
REMOVE_FUNCTION_FROM_ROLE
}- GuardController documentation for function schema management.
import { encodeAbiParameters } from 'viem'
// Define function permissions for the role
const functionPermissions = [
{
functionSelector: '0xa9059cbb', // transfer(address,uint256)
grantedActionsBitmap: 0b000000111, // EXECUTE_TIME_DELAY_REQUEST, APPROVE, CANCEL
handlerForSelectors: ['0x00000000'] // bytes4(0) for execution selector
}
]
// Create batch action
const createRoleAction = {
actionType: 'CREATE_ROLE',
data: encodeAbiParameters(
['string', 'uint256', 'tuple[]'],
[
'TreasuryManager',
5, // maxWallets
functionPermissions
]
)
}
// Create meta-transaction for batch
const metaTxParams = await runtimeRBAC.createMetaTxParams(
contractAddress,
'0x...', // roleConfigBatchRequestAndApprove selector
TxAction.SIGN_META_REQUEST_AND_APPROVE,
24n * 60n * 60n, // 24 hour deadline
BigInt('50000000000'), // max gas price
ownerAddress
)
const metaTx = await runtimeRBAC.generateUnsignedMetaTransactionForNew(
ownerAddress,
contractAddress,
0n, // value
0n, // gas limit
keccak256('ROLE_CONFIG_BATCH'), // operation type
'0x...', // executeRoleConfigBatch selector
encodeAbiParameters(
['tuple[]'],
[[createRoleAction]]
),
metaTxParams
)
// Sign the meta-transaction
const signature = await walletClient.signMessage({
message: { raw: metaTx.message },
account: ownerAddress
})
// Execute via broadcaster
const txHash = await runtimeRBAC.roleConfigBatchRequestAndApprove(
{ ...metaTx, signature },
{ from: broadcasterAddress }
)const addWalletAction = {
actionType: RoleConfigActionType.ADD_WALLET,
data: encodeAbiParameters(
['bytes32', 'address'],
[roleHash, walletAddress]
)
}
// Create and execute batch (similar to create role example)Note: The function schema must be registered via GuardController before adding permissions.
const addFunctionToRoleAction = {
actionType: RoleConfigActionType.ADD_FUNCTION_TO_ROLE,
data: encodeAbiParameters(
['bytes32', 'tuple'],
[
roleHash,
{
functionSelector: '0xa9059cbb',
grantedActionsBitmap: 0b000000111,
handlerForSelectors: ['0x00000000']
}
]
)
}
// Create and execute batch (similar to create role example)Contracts emit ComponentEvent(bytes4 functionSelector, bytes data). For RBAC config, filter by executeRoleConfigBatch selector and decode data as (RoleConfigActionType, bytes32 roleHash, bytes4 functionSelector, address wallet). See contract API and NatSpec.
const unwatch = publicClient.watchContractEvent({
address: contractAddress,
abi: runtimeRBAC.abi,
eventName: 'ComponentEvent',
onLogs: (logs) => {
logs.forEach(log => {
if (log.args.functionSelector === executeRoleConfigBatchSelector) {
const decoded = decodeAbiParameters(/* ... */, log.args.data)
console.log('Role config:', decoded)
}
})
}
})
unwatch()Protected roles (OWNER_ROLE, BROADCASTER_ROLE, RECOVERY_ROLE) cannot be removed:
const role = await runtimeRBAC.getRole(roleHash)
if (role.isProtected) {
console.log('This role is protected and cannot be removed')
}Roles have maximum wallet limits:
const role = await runtimeRBAC.getRole(roleHash)
const wallets = await runtimeRBAC.getAuthorizedWallets(roleHash)
if (wallets.length >= role.maxWallets) {
throw new Error('Role has reached maximum wallet limit')
}Fine-grained permission control with action-level permissions:
// Check specific action permission
const canRequest = await runtimeRBAC.hasActionPermission(
account,
'0xa9059cbb', // transfer function selector
TxAction.EXECUTE_TIME_DELAY_REQUEST
)
if (!canRequest) {
throw new Error('Account does not have request permission')
}Protected function schemas cannot be unregistered:
const schema = await runtimeRBAC.getFunctionSchema(functionSelector)
if (schema.isProtected) {
console.log('This function schema is protected and cannot be unregistered')
}// Create multiple roles in a single batch
const actions = [
{
actionType: 'CREATE_ROLE',
data: encodeAbiParameters(
['string', 'uint256', 'tuple[]'],
['ADMIN_ROLE', 3, adminPermissions]
)
},
{
actionType: 'CREATE_ROLE',
data: encodeAbiParameters(
['string', 'uint256', 'tuple[]'],
['MODERATOR_ROLE', 10, moderatorPermissions]
)
},
{
actionType: 'ADD_WALLET',
data: encodeAbiParameters(
['bytes32', 'address'],
[adminRoleHash, adminAddress]
)
}
]
// Execute batch via meta-transactionNote: Function schema registration is now handled by GuardController. Use GuardController's guardConfigBatchRequestAndApprove with the REGISTER_FUNCTION action.
import { GuardController, GuardConfigActionType } from '@bloxchain/sdk/typescript'
// Step 1: Register function schema via GuardController
const registerAction = {
actionType: GuardConfigActionType.REGISTER_FUNCTION,
data: encodeAbiParameters(
['string', 'string', 'uint8[]'],
[
'withdraw(address,uint256)',
'WITHDRAW_OPERATION',
[TxAction.EXECUTE_TIME_DELAY_REQUEST, TxAction.EXECUTE_TIME_DELAY_APPROVE]
]
)
}
// Execute via GuardController
await guardController.guardConfigBatchRequestAndApprove(metaTx, { from: broadcasterAddress })
// Step 2: Add function permission to role via RuntimeRBAC
const addPermissionAction = {
actionType: RoleConfigActionType.ADD_FUNCTION_TO_ROLE,
data: encodeAbiParameters(
['bytes32', 'tuple'],
[
roleHash,
{
functionSelector: '0x...', // withdraw selector
grantedActionsBitmap: 0b000000011,
handlerForSelectors: ['0x00000000']
}
]
)
}
// Execute via RuntimeRBAC
await runtimeRBAC.roleConfigBatchRequestAndApprove(metaTx, { from: broadcasterAddress })import { describe, it, expect } from 'vitest'
describe('RuntimeRBAC', () => {
it('should return correct role information', async () => {
const role = await runtimeRBAC.getRole(roleHash)
expect(role.roleHash).toBe(roleHash)
expect(role.maxWallets).toBeGreaterThan(0)
})
it('should check role membership', async () => {
const hasRole = await runtimeRBAC.hasRole(roleHash, account)
expect(typeof hasRole).toBe('boolean')
})
it('should get roles for wallet', async () => {
const walletRoles = await runtimeRBAC.getWalletRoles(account)
expect(Array.isArray(walletRoles)).toBe(true)
})
it('should get wallets in role', async () => {
const wallets = await runtimeRBAC.getAuthorizedWallets(roleHash)
expect(Array.isArray(wallets)).toBe(true)
})
})describe('RuntimeRBAC Integration', () => {
it('should complete role creation workflow', async () => {
// Create role via batch
const createAction = {
actionType: 'CREATE_ROLE',
data: encodeAbiParameters(
['string', 'uint256', 'tuple[]'],
['TEST_ROLE', 5, []]
)
}
// Execute batch via meta-transaction
const txHash = await runtimeRBAC.roleConfigBatchRequestAndApprove(
metaTx,
{ from: broadcasterAddress }
)
// Verify role exists
const role = await runtimeRBAC.getRole(roleHash)
expect(role.roleName).toBe('TEST_ROLE')
})
})Solution: Protected roles (OWNER_ROLE, BROADCASTER_ROLE, RECOVERY_ROLE) cannot be removed. Use a different role or create a new one.
Solution: Increase the role's wallet limit or revoke roles from other accounts before adding new ones.
Solution: Register the function schema via GuardController before adding it to a role. Use GuardController's guardConfigBatchRequestAndApprove with REGISTER_FUNCTION action.
Solution: Ensure the account has the required role and function permissions. Check with hasActionPermission.
Solution: Use the correct role hash. Generate it using keccak256(abi.encodePacked(roleName)).
Solution: Ensure handlerForSelectors array in function permission matches the function schema's handlerForSelectors array. Use bytes4(0) for execution selectors.
Solution: Re-adding the same selector with RoleConfigActionType.ADD_FUNCTION_TO_ROLE hits the same ResourceAlreadyExists revert as a direct addFunctionToRole would when the selector is already on the role. To change the stored permission bitmap or handlerForSelectors, include RoleConfigActionType.REMOVE_FUNCTION_FROM_ROLE in a roleConfigBatchRequestAndApprove → executeRoleConfigBatch flow first, then ADD_FUNCTION_TO_ROLE with the new values—the engine applies removal through that batch path; there is no supported separate public removeFunctionFromRole entrypoint for integrators. Note: protected schemas cannot be removed from roles (CannotModifyProtected), so grants of protected selectors are effectively permanent unless the role itself is removed.
- API Reference - Complete API documentation
- SecureOwnable Guide - Base contract functionality
- State Machine Engine - State machine architecture
- Best Practices - Development guidelines