> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://docs.prividium.com/api/mcp` to find what you need.
>
> **Have feedback?** Use `submit_feedback` on the same MCP server.

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.

:::info[IMPORTANT]
Admin accounts with passkey-based authentication enabled are not suitable for programmatic RPC access.
:::

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](/developers/quickstart/deploying-contracts).

## 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.

```bash
touch scripts/config.ts
```

Copy and paste the permissions configuration below.

**config.ts**

```typescript
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 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 has `full_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 the `checkRole` and `restrictArgument` rules so that both rules must be
  satisfied.
* `checkRoleOrRestrictArgument`: Combines the `checkRole` and `restrictArgument` rules so that either rule must be
  satisfied.

:::note[Admins can bypass these configurations.]
Admins and users with `full_sequencer_rpc_access` can bypass configured permissions to read or write to any contract.
:::

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.

```bash
touch scripts/utils.ts
```

Then, copy and paste the utils file below.

**utils.ts**

```typescript
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.

```bash
touch scripts/setup-permissions.ts
```

Copy and paste the setup file below.

**setup-permissions.ts**

```typescript
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`:

```bash
npx hardhat run scripts/setup-permissions.ts --network localPrividium
```

```bash
npx hardhat run scripts/setup-permissions.ts --network testnetPrividium
```

After 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.
