GuidesResolver Development

Resolver Development Guide

How to create custom resolvers that extend Integra’s functionality.

Overview

Resolvers are the primary extensibility mechanism in Integra. By implementing the IDocumentResolver interface, you can add custom functionality to documents without modifying core contracts. This guide walks you through creating, testing, and deploying custom resolvers.

The IDocumentResolver Interface

All resolvers implement this interface:

interface IDocumentResolver {
    /// @notice Called when a document is first registered
    function onDocumentRegistered(
        bytes32 integraHash,
        bytes32 documentHash,
        address owner,
        bytes calldata metadata
    ) external;
 
    /// @notice Called when document ownership transfers
    function onOwnershipTransferred(
        bytes32 integraHash,
        address oldOwner,
        address newOwner,
        string calldata reason
    ) external;
 
    /// @notice Called when a tokenizer is associated
    function onTokenizerAssociated(
        bytes32 integraHash,
        address tokenizer,
        address owner
    ) external;
 
    /// @notice Called on document updates
    function onDocumentUpdated(
        bytes32 integraHash,
        bytes calldata updateData
    ) external;
}

Creating Your First Resolver

Step 1: Basic Structure

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
 
import "../interfaces/IDocumentResolver.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
 
contract MyCustomResolver is
    IDocumentResolver,
    OwnableUpgradeable,
    UUPSUpgradeable
{
    // Your storage variables
    mapping(bytes32 => bytes) private documentData;
 
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }
 
    function initialize() public initializer {
        __Ownable_init(msg.sender);
        __UUPSUpgradeable_init();
    }
 
    function _authorizeUpgrade(address newImplementation)
        internal override onlyOwner {}
 
    // Implement interface methods...
}

Step 2: Implement Hooks

function onDocumentRegistered(
    bytes32 integraHash,
    bytes32 documentHash,
    address owner,
    bytes calldata metadata
) external override {
    // Validate caller is document registry (optional but recommended)
    require(msg.sender == DOCUMENT_REGISTRY, "Unauthorized");
 
    // Store custom data
    documentData[integraHash] = metadata;
 
    // Emit event for off-chain tracking
    emit DocumentProcessed(integraHash, owner);
}
 
function onOwnershipTransferred(
    bytes32 integraHash,
    address oldOwner,
    address newOwner,
    string calldata reason
) external override {
    // Your transfer logic
    emit OwnershipUpdated(integraHash, oldOwner, newOwner, reason);
}
 
function onTokenizerAssociated(
    bytes32 integraHash,
    address tokenizer,
    address owner
) external override {
    // Your tokenizer association logic
}
 
function onDocumentUpdated(
    bytes32 integraHash,
    bytes calldata updateData
) external override {
    // Your update logic
    documentData[integraHash] = updateData;
}

Step 3: Add Query Functions

/// @notice Query stored data
function getDocumentData(bytes32 integraHash)
    external view returns (bytes memory)
{
    return documentData[integraHash];
}
 
/// @notice Check if document is processed
function isProcessed(bytes32 integraHash)
    external view returns (bool)
{
    return documentData[integraHash].length > 0;
}

Common Resolver Patterns

Compliance Resolver

contract ComplianceResolver is IDocumentResolver {
    mapping(address => bool) public accreditedInvestors;
 
    function onDocumentRegistered(
        bytes32 integraHash,
        bytes32,
        address owner,
        bytes calldata
    ) external override {
        // Require owner to be accredited
        require(accreditedInvestors[owner], "Not accredited");
    }
 
    function onOwnershipTransferred(
        bytes32,
        address,
        address newOwner,
        string calldata
    ) external override {
        // Require new owner to be accredited
        require(accreditedInvestors[newOwner], "New owner not accredited");
    }
 
    // Admin function to add accredited investors
    function addAccreditedInvestor(address investor)
        external onlyOwner
    {
        accreditedInvestors[investor] = true;
    }
}

Contact Information Resolver

contract ContactResolver is IDocumentResolver {
    struct ContactInfo {
        bytes32 encryptedEmail;
        bytes32 encryptedPhone;
        bytes publicKey;
    }
 
    mapping(bytes32 => ContactInfo) private contacts;
 
    function onDocumentRegistered(
        bytes32 integraHash,
        bytes32,
        address,
        bytes calldata metadata
    ) external override {
        ContactInfo memory info = abi.decode(metadata, (ContactInfo));
        contacts[integraHash] = info;
    }
 
    function getContact(bytes32 integraHash)
        external view returns (ContactInfo memory)
    {
        return contacts[integraHash];
    }
}

Lifecycle Tracking Resolver

contract LifecycleResolver is IDocumentResolver {
    struct Lifecycle {
        uint256 createdAt;
        uint256 expiresAt;
        bool isExpired;
    }
 
    mapping(bytes32 => Lifecycle) public lifecycles;
 
    function onDocumentRegistered(
        bytes32 integraHash,
        bytes32,
        address,
        bytes calldata metadata
    ) external override {
        uint256 duration = abi.decode(metadata, (uint256));
 
        lifecycles[integraHash] = Lifecycle({
            createdAt: block.timestamp,
            expiresAt: block.timestamp + duration,
            isExpired: false
        });
    }
 
    function checkExpiry(bytes32 integraHash) external {
        Lifecycle storage lifecycle = lifecycles[integraHash];
        if (block.timestamp > lifecycle.expiresAt && !lifecycle.isExpired) {
            lifecycle.isExpired = true;
            emit DocumentExpired(integraHash);
        }
    }
}

Testing Your Resolver

Unit Tests

import { expect } from 'chai';
import { ethers } from 'hardhat';
 
describe('MyCustomResolver', () => {
    let resolver: Contract;
    let registry: Contract;
 
    beforeEach(async () => {
        // Deploy resolver
        const Resolver = await ethers.getContractFactory('MyCustomResolver');
        resolver = await upgrades.deployProxy(Resolver);
 
        // Mock registry for testing
        registry = await deployMockRegistry();
    });
 
    it('should process document registration', async () => {
        const integraHash = ethers.utils.randomBytes(32);
        const metadata = ethers.utils.defaultAbiCoder.encode(
            ['string'],
            ['test metadata']
        );
 
        await resolver.connect(registry).onDocumentRegistered(
            integraHash,
            ethers.constants.HashZero,
            owner.address,
            metadata
        );
 
        expect(await resolver.isProcessed(integraHash)).to.be.true;
    });
});

Integration Tests

it('should work with real document registry', async () => {
    // Register resolver in component registry
    await componentRegistry.registerComponent(
        RESOLVER_TYPE,
        'MyResolver',
        resolver.address,
        await getCodeHash(resolver.address)
    );
 
    // Register document with resolver
    const tx = await documentRegistry.registerDocument(
        documentHash,
        referenceHash,
        tokenizer,
        executor,
        processHash,
        identityExtension,
        myResolverId,
        []
    );
 
    // Verify resolver was called
    const integraHash = await getIntegrahashFromTx(tx);
    expect(await resolver.isProcessed(integraHash)).to.be.true;
});

Deployment

Deploy to Testnet

import { ethers, upgrades } from 'hardhat';
 
async function main() {
    const Resolver = await ethers.getContractFactory('MyCustomResolver');
 
    const resolver = await upgrades.deployProxy(Resolver, [], {
        kind: 'uups',
    });
 
    await resolver.waitForDeployment();
    console.log('Resolver deployed to:', await resolver.getAddress());
 
    // Register in component registry
    const componentRegistry = await ethers.getContractAt(
        'IntegraRegistry',
        COMPONENT_REGISTRY_ADDRESS
    );
 
    await componentRegistry.registerComponent(
        RESOLVER_TYPE,
        'MyCustomResolver',
        await resolver.getAddress(),
        await getCodeHash(await resolver.getAddress())
    );
}

Best Practices

Security

  1. Validate caller - Ensure only authorized contracts call hooks
  2. Use reentrancy guards - Protect state-changing functions
  3. Limit gas consumption - Stay within gas limits
  4. Handle failures gracefully - Don’t break document operations

Design

  1. Keep it focused - One resolver, one responsibility
  2. Use events - Enable off-chain indexing
  3. Plan for upgrades - Use UUPS pattern
  4. Document thoroughly - Help others integrate

Testing

  1. Test all hooks - Each lifecycle event
  2. Test edge cases - Empty data, invalid inputs
  3. Test gas consumption - Ensure within limits
  4. Integration test - With real registry