Configuring Contracts
Now your token contract is deployed, but it can't be called by standard users because the permissions need to be configured still.
All contracts deployed to a Prividium network must be configured with specific permissions for each function inside the
contract, including read and write operations. All contract functions default to "Forbidden" until configured. Only
users with full_sequencer_rpc_access can read or write to the contract.
You can either setup the contract permissions manually via the admin panel, or programmatically. In this guide we will use the programmatic option by creating a script to register the contract with the admin panel and configure those permissions. Registering the contract here just means making the admin panel aware that the contract exists. In the admin panel, this would be equivalent to adding a new contract name, address, and ABI.
If you don't have admin access in the Prividium instance you are using, you'll need to ask an admin to set this up for you. Otherwise, you can follow the steps below.
This guide assumes you still have the Prividium proxy running and are using the same hardhat.config.ts file set up in
the contract deployment guide.
Creating the permissions config
Create a new file in the scripts folder called config.ts. This is where we will define the specific permissions for
each function inside the token contract.
touch scripts/config.tsCopy and paste the permissions configuration below.
export type PermissionRuleType =
| 'forbidden'
| 'public'
| 'checkRole'
| 'restrictArgument'
| 'checkRoleAndRestrictArgument'
| 'checkRoleOrRestrictArgument';
export type ContractArgumentRestrictionConfig = {
kind: 'caller_address_match';
argumentIndex: number;
};
export type ContractPermissionRuleConfig = {
ruleType: PermissionRuleType;
roles?: string[];
argumentRestrictions?: ContractArgumentRestrictionConfig[];
};
export type ContractPermissionConfig = Record<string, ContractPermissionRuleConfig>;
export const tokenPermissionConfig: ContractPermissionConfig = {
'allowance(address,address)': {
ruleType: 'public'
},
'approve(address,uint256)': {
ruleType: 'public'
},
'balanceOf(address)': {
ruleType: 'checkRoleOrRestrictArgument',
roles: ['admin'],
argumentRestrictions: [
{
kind: 'caller_address_match',
argumentIndex: 0
}
]
},
'burn(uint256)': {
ruleType: 'checkRole',
roles: ['admin']
},
'burnFrom(address,uint256)': {
ruleType: 'checkRole',
roles: ['admin']
},
'decimals()': {
ruleType: 'public'
},
'mint(address,uint256)': {
ruleType: 'checkRole',
roles: ['admin']
},
'name()': {
ruleType: 'public'
},
'owner()': {
ruleType: 'checkRole',
roles: ['admin']
},
'renounceOwnership()': {
ruleType: 'checkRole',
roles: ['admin']
},
'symbol()': {
ruleType: 'public'
},
'totalSupply()': {
ruleType: 'checkRole',
roles: ['admin']
},
'transfer(address,uint256)': {
ruleType: 'public'
},
'transferFrom(address,address,uint256)': {
ruleType: 'public'
},
'transferOwnership(address)': {
ruleType: 'checkRole',
roles: ['admin']
}
};As defined in the PermissionRuleType, there are six types of permission types you can select for a function.
forbidden: This is the default permission for all functions. Functions with this rule cannot be called unless the user hasfull_sequencer_rpc_access.public: All users registered with the network can call this function without restriction.checkRole: Only users with one of the selected roles can call this function.restrictArgument: The function can only be called if a certain argument matches the caller's wallet address.checkRoleAndRestrictArgument: Combines thecheckRoleandrestrictArgumentrules so that both rules must be satisfied.checkRoleOrRestrictArgument: Combines thecheckRoleandrestrictArgumentrules so that either rule must be satisfied.
The token contract in this example will be configured so that:
- All users can read the name, decimals, and symbol of the token.
- All users can approve the token to be spent, check the allowance, and transfer tokens.
- Only admins can read the total supply, mint or burn the token, read the owner address, or change ownership.
- Users can only read the balance of their own wallet address, while admins can read the balance of all wallet addresses.
Adding utility functions
Create a new file in the scripts folder called utils.ts. This is where we will add some helper functions to
authenticate a session programmatically, register the contract with the admin panel, and apply the contract permissions
configurations.
touch scripts/utils.tsThen, copy and paste the utils file below.
import { network } from 'hardhat';
import { type Abi, type Address, type Hex, toFunctionSelector } from 'viem';
import type { ContractArgumentRestrictionConfig, ContractPermissionConfig } from './config.js';
const DEFAULT_PRIVIDIUM_BASE_URL = 'http://localhost:8000/api';
const DEFAULT_PRIVIDIUM_AUTH_BASE_URL = 'localhost:3001';
type AbiFunction = Extract<Abi[number], { type: 'function' }>;
type ContractDetails = {
id: string;
contractAddress: string;
name: string;
description: string;
abi: string;
};
type ApiClientConfig = {
baseUrl: string;
headers?: Record<string, string>;
};
type ApiPermissionRuleType =
| 'public'
| 'checkRole'
| 'restrictArgument'
| 'checkRoleAndRestrictArgument'
| 'checkRoleOrRestrictArgument';
type ApiResponse<T> = {
response: Response;
data?: T;
error?: unknown;
};
type ContractPermissionList = {
items: Array<{ id: string }>;
total?: number;
};
type RoleList = {
items: Array<{ id: string; roleName: string }>;
};
type SignatureParam = {
type: string;
components?: readonly SignatureParam[];
};
const DEFAULT_PERMISSION_RULE = {
ruleType: 'forbidden' as const,
roles: [],
argumentRestrictions: []
};
export class ApiClient {
constructor(private readonly config: ApiClientConfig) {}
async request<T>(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body?: unknown): Promise<ApiResponse<T>> {
const response = await fetch(`${this.config.baseUrl}${path}`, {
method,
headers: {
'Content-Type': 'application/json',
...this.config.headers
},
body: body ? JSON.stringify(body) : undefined
});
if (!response.ok) {
const error = await response.json().catch(() => ({
message: response.statusText,
status: response.status
}));
return { response, error };
}
const data = (await response.json()) as T;
return { response, data };
}
get<T>(path: string) {
return this.request<T>('GET', path);
}
post<T>(path: string, body: unknown) {
return this.request<T>('POST', path, body);
}
put<T>(path: string, body: unknown) {
return this.request<T>('PUT', path, body);
}
delete<T>(path: string) {
return this.request<T>('DELETE', path);
}
}
export async function ensureContractRegistration(
client: ApiClient,
contract: {
name: string;
description: string;
address: Address;
abi: Abi;
templateKey?: string;
permissionConfig?: ContractPermissionConfig;
}
) {
let contractRegistered = false;
let permissionRegistrationFailed = false;
try {
const existing = await getContractByAddress(client, contract.address);
if (existing.response.status === 404) {
extractRes(
await client.post('/contracts/', {
abi: JSON.stringify(contract.abi),
name: contract.name,
contractAddress: contract.address,
description: contract.description,
...(contract.templateKey ? { templateKey: contract.templateKey } : {}),
disclosureStartBlock: '0x0',
discloseBytecode: false,
discloseErc20Balance: false,
discloseErc20TotalSupply: false,
erc20LockAddresses: []
})
);
} else {
extractRes(existing);
console.log(`${contract.name} already registered in Prividium.`);
}
contractRegistered = true;
} catch (error) {
console.error(`Failed to register ${contract.name} in Prividium:`, error);
}
if (!contractRegistered) {
console.log(`Skipping permission configuration for ${contract.name}.`);
return;
}
if (!contract.permissionConfig) {
console.log(`No permission config found for ${contract.name}.`);
return;
}
const roleIdMap = await resolveRoleIds(client, contract.permissionConfig);
for (const abiItem of contract.abi) {
if (abiItem.type !== 'function') {
continue;
}
const methodSelector = toFunctionSelector(abiItem);
const functionLookupSignature = formatFunctionSignature(abiItem);
const functionSignature = formatFunctionSignatureForApi(abiItem);
const permissionRule = contract.permissionConfig?.[functionLookupSignature] ?? DEFAULT_PERMISSION_RULE;
try {
const normalizedRuleType = normalizeRuleType(permissionRule.ruleType);
const existingPermission = extractRes(
await getContractPermissions(client, contract.address, methodSelector)
);
const existingPermissionId = existingPermission.items[0]?.id;
if (normalizedRuleType === null) {
if (existingPermissionId) {
extractRes(await client.delete(`/contract-permissions/${existingPermissionId}`));
}
continue;
}
const payload = {
contractAddress: contract.address,
accessType: abiItem.stateMutability === 'view' || abiItem.stateMutability === 'pure' ? 'read' : 'write',
argumentRestrictions: buildArgumentRestrictions(permissionRule.argumentRestrictions),
roles: buildRoleBindings(permissionRule.roles, roleIdMap),
functionSignature,
methodSelector,
ruleType: normalizedRuleType
};
if (existingPermissionId) {
extractRes(await client.put(`/contract-permissions/${existingPermissionId}`, payload));
continue;
}
extractRes(await client.post('/contract-permissions/', payload));
} catch (error) {
permissionRegistrationFailed = true;
console.error(
`Failed to register permission for ${contract.name}.${abiItem.name} (${functionSignature}) with selector ${methodSelector}:`,
error
);
}
}
if (permissionRegistrationFailed) {
throw new Error(`Failed to configure one or more function permissions for ${contract.name}.`);
}
}
export async function initAuthSession() {
const connection = await network.create();
const { viem, networkName } = connection;
const [walletClient] = await viem.getWalletClients();
if (!walletClient?.account) {
throw new Error(`No wallet client configured for ${networkName}.`);
}
const walletAddress = walletClient.account.address;
const signMessage = (message: string) => walletClient.signMessage({ account: walletClient.account, message });
console.log(`Authenticating to Prividium API on ${networkName}...`);
const apiClient = await createAuthSession(
DEFAULT_PRIVIDIUM_BASE_URL,
DEFAULT_PRIVIDIUM_AUTH_BASE_URL,
walletAddress,
signMessage
);
console.log('✅ Successfully authenticated.');
return apiClient;
}
async function createAuthSession(
baseUrl: string,
siweDomain: string,
walletAddress: Address,
signMessage: (message: string) => Promise<Hex>
) {
const client = new ApiClient({ baseUrl });
try {
const challenge = extractRes(
await client.post<{ msg?: string; message?: string; nonceToken?: string }>('/siwe-messages/', {
address: walletAddress,
domain: siweDomain
})
);
const message = challenge.message ?? challenge.msg;
if (!message) {
throw new Error(`SIWE challenge missing message field. Response: ${JSON.stringify(challenge)}`);
}
if (!challenge.nonceToken) {
throw new Error(`SIWE challenge missing nonceToken. Response: ${JSON.stringify(challenge)}`);
}
const signature = await signMessage(message);
const { token } = extractRes(
await client.post<{ token: string }>('/auth/login/crypto-native', {
message,
signature,
nonceToken: challenge.nonceToken
})
);
return new ApiClient({
baseUrl,
headers: { authorization: `Bearer ${token}` }
});
} catch (error) {
console.log('ERROR:', error);
throw new Error(
`Unable to authenticate with Prividium API using base URL "${baseUrl}". ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
export async function getContractByAddress(client: ApiClient, address: Address) {
return client.get<ContractDetails>(`/contracts/${encodeURIComponent(address)}`);
}
export function extractRes<T>(response: ApiResponse<T>): T {
if (response.error !== undefined) {
throw new Error(JSON.stringify(response.error, null, 2));
}
if (response.data === undefined) {
throw new Error('No data received from API');
}
return response.data;
}
function buildArgumentRestrictions(restrictions: ContractArgumentRestrictionConfig[] | undefined) {
return (restrictions ?? []).map((restriction) => {
if (restriction.kind === 'caller_address_match') {
return {
argumentIndex: restriction.argumentIndex,
operator: 'eq',
valueSource: 'caller_address'
};
}
throw new Error(
`Unsupported argument restriction kind: ${(restriction as { kind?: string }).kind ?? 'unknown'}`
);
});
}
async function resolveRoleIds(client: ApiClient, permissionConfig: ContractPermissionConfig) {
const names = [...new Set(Object.values(permissionConfig).flatMap((r) => r.roles ?? []))];
const map = new Map<string, string>();
for (const name of names) {
const { items } = extractRes(
await client.get<RoleList>(`/roles/?searchQuery=${encodeURIComponent(name)}&limit=100`)
);
const match = items.find((r) => r.roleName === name);
if (!match) throw new Error(`Role "${name}" not found. Create it before registering permissions.`);
map.set(name, match.id);
}
return map;
}
function buildRoleBindings(roles: string[] | undefined, roleIdMap: Map<string, string>) {
return (roles ?? []).map((roleName) => ({ id: roleIdMap.get(roleName)!, roleName }));
}
function formatFunctionSignatureForApi(abiItem: AbiFunction): string {
const params = abiItem.inputs.map((input) => formatParamType(input)).join(', ');
const returns = (abiItem.outputs ?? []).map((output) => formatParamType(output)).join(', ');
const mutability = ['view', 'pure', 'payable'].includes(abiItem.stateMutability)
? ` ${abiItem.stateMutability}`
: '';
const returnsPart = returns ? ` returns (${returns})` : '';
return `function ${abiItem.name}(${params})${mutability}${returnsPart}`;
}
function formatFunctionSignature(abiItem: AbiFunction): string {
const params = abiItem.inputs.map((input) => formatParamType(input)).join(',');
return `${abiItem.name}(${params})`;
}
function formatParamType(param: SignatureParam): string {
if (!param.type.startsWith('tuple')) {
return param.type;
}
const suffix = param.type.slice('tuple'.length);
const tupleMembers = (param.components ?? [])
.map((component: SignatureParam) => formatParamType(component))
.join(',');
return `(${tupleMembers})${suffix}`;
}
async function getContractPermissions(client: ApiClient, contractAddress: Address, methodSelector: Hex) {
const query = new URLSearchParams({
contractAddress,
methodSelector,
limit: '1',
offset: '0'
});
return client.get<ContractPermissionList>(`/contract-permissions/?${query.toString()}`);
}
function normalizeRuleType(ruleType: ContractPermissionConfig[string]['ruleType']): ApiPermissionRuleType | null {
return ruleType === 'forbidden' ? null : ruleType;
}If you want to run this script on a testnet or mainnet, make sure to change the default URLs at the top of the file. Otherwise if you are running this on a local instance of Prividium, use the default urls here.
Create the setup script
Create a new file in the scripts folder called setup-permissions.ts. This is where we will call the
ensureContractRegistration function to register and configure the token contract.
touch scripts/setup-permissions.tsCopy and paste the setup file below.
import { artifacts } from 'hardhat';
import { isAddress } from 'viem';
import { tokenPermissionConfig } from './config.js';
import { ensureContractRegistration, initAuthSession } from './utils.js';
// Replace with your deployed token address
const TOKEN_CONTRACT_ADDRESS = process.env.TOKEN_CONTRACT_ADDRESS ?? '0x...';
async function main() {
if (!isAddress(TOKEN_CONTRACT_ADDRESS)) {
throw new Error(
`Invalid TOKEN_CONTRACT_ADDRESS "${TOKEN_CONTRACT_ADDRESS}". Replace the placeholder in scripts/setup-permissions.ts with a deployed 0x... address.`
);
}
const client = await initAuthSession();
const abi = (await artifacts.readArtifact('QuickstartToken')).abi;
await ensureContractRegistration(client, {
name: 'Quickstart Token',
description: 'Token contract from Prividium quickstart.',
address: TOKEN_CONTRACT_ADDRESS,
abi,
permissionConfig: tokenPermissionConfig
});
console.log('✅ Successfully configured contract permissions.');
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});Running the setup script
Replace the TOKEN_CONTRACT_ADDRESS at the top of the file with your deployed token contract address. (If you forgot
this, you can find it in the ignition/deployments folder.)
Now you can run the script.
Choose either localPrividium or testnetPrividium:
npx hardhat run scripts/setup-permissions.ts --network localPrividiumnpx hardhat run scripts/setup-permissions.ts --network testnetPrividiumAfter running the script, you should now see the contract appear in the "Contracts" tab of the admin panel, configured
with the same permissions that were used in the config.ts file.