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
- Validate caller - Ensure only authorized contracts call hooks
- Use reentrancy guards - Protect state-changing functions
- Limit gas consumption - Stay within gas limits
- Handle failures gracefully - Don’t break document operations
Design
- Keep it focused - One resolver, one responsibility
- Use events - Enable off-chain indexing
- Plan for upgrades - Use UUPS pattern
- Document thoroughly - Help others integrate
Testing
- Test all hooks - Each lifecycle event
- Test edge cases - Empty data, invalid inputs
- Test gas consumption - Ensure within limits
- Integration test - With real registry
Related Documentation
- Resolver Composition - Resolver architecture
- Extensibility Overview - Extension patterns
- Security Patterns - Security considerations
- Integration Guide - Integration walkthrough