Best Practices for Entity Integration
Recommended patterns, security considerations, and optimization strategies for working with Endaoment entities.Security Best Practices
Always Verify Entity Status
Before any transaction, confirm the entity is active:Copy
Ask AI
async function safeEntityInteraction(entityAddress: string): Promise<boolean> {
const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
const registry = new ethers.Contract(REGISTRY_ADDRESS, REGISTRY_ABI, provider);
// 1. Check if entity exists and is active
const isActive = await registry.isActiveEntity(entityAddress);
if (!isActive) {
throw new Error(`Entity ${entityAddress} is not active or does not exist`);
}
// 2. Verify contract has code
const code = await provider.getCode(entityAddress);
if (code === '0x') {
throw new Error('No contract at this address');
}
// 3. Optional: Verify it's the correct entity type
const ENTITY_ABI = ['function entityType() external pure returns (uint8)'];
const entity = new ethers.Contract(entityAddress, ENTITY_ABI, provider);
const entityType = await entity.entityType();
console.log(`✓ Entity verified: ${entityType === 0 ? 'Org' : 'Fund'}`);
return true;
}
Validate Amounts
Always validate amounts before transactions:Copy
Ask AI
function validateAmount(amount: string, decimals: number = 6): ethers.BigNumber {
// 1. Check it's a valid number
if (isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) {
throw new Error('Invalid amount: must be a positive number');
}
// 2. Check precision
const decimalPlaces = amount.split('.')[1]?.length || 0;
if (decimalPlaces > decimals) {
throw new Error(`Too many decimal places (max ${decimals})`);
}
// 3. Convert to Wei
const amountWei = ethers.utils.parseUnits(amount, decimals);
// 4. Sanity check (e.g., not more than $1B)
const maxAmount = ethers.utils.parseUnits('1000000000', decimals);
if (amountWei.gt(maxAmount)) {
throw new Error('Amount too large');
}
return amountWei;
}
// Usage
try {
const amount = validateAmount('1000.50');
await entity.donate(amount);
} catch (error) {
console.error(`Validation failed: ${error.message}`);
}
Check Permissions Before Operations
Copy
Ask AI
async function verifyPermissions(
entityAddress: string,
walletAddress: string,
operation: 'donate' | 'transfer' | 'manager'
): Promise<boolean> {
const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
const entity = new ethers.Contract(entityAddress, ENTITY_ABI, provider);
if (operation === 'donate') {
// Anyone can donate
return true;
}
if (operation === 'transfer' || operation === 'manager') {
// Check if wallet is manager
const manager = await entity.manager();
if (manager.toLowerCase() === walletAddress.toLowerCase()) {
return true;
}
console.log('✗ Not the entity manager');
return false;
}
return false;
}
Error Handling
Comprehensive Error Handler
Copy
Ask AI
class EntityOperationError extends Error {
constructor(
message: string,
public code: string,
public details?: any
) {
super(message);
this.name = 'EntityOperationError';
}
}
async function safeDonation(
entityAddress: string,
amount: string
): Promise<ethers.ContractReceipt> {
const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
try {
// 1. Validate inputs
const amountWei = validateAmount(amount);
// 2. Verify entity
await safeEntityInteraction(entityAddress);
// 3. Check USDC balance
const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, signer);
const balance = await usdc.balanceOf(signer.address);
if (balance.lt(amountWei)) {
throw new EntityOperationError(
'Insufficient USDC balance',
'INSUFFICIENT_BALANCE',
{
required: ethers.utils.formatUnits(amountWei, 6),
available: ethers.utils.formatUnits(balance, 6)
}
);
}
// 4. Check allowance
const allowance = await usdc.allowance(signer.address, entityAddress);
if (allowance.lt(amountWei)) {
console.log('Approving USDC...');
const approveTx = await usdc.approve(entityAddress, amountWei);
await approveTx.wait();
}
// 5. Execute donation
const entity = new ethers.Contract(entityAddress, ENTITY_ABI, signer);
const tx = await entity.donate(amountWei, {
gasLimit: 200000
});
const receipt = await tx.wait();
console.log(`✓ Donation successful: ${receipt.transactionHash}`);
return receipt;
} catch (error) {
// Handle different error types
if (error instanceof EntityOperationError) {
console.error(`❌ ${error.message}`);
console.error(`Code: ${error.code}`);
if (error.details) {
console.error('Details:', error.details);
}
} else if (error.code === 'INSUFFICIENT_FUNDS') {
console.error('❌ Insufficient ETH for gas');
} else if (error.code === 'UNPREDICTABLE_GAS_LIMIT') {
console.error('❌ Transaction would fail. Check entity status and parameters.');
} else {
console.error('❌ Unexpected error:', error.message);
}
throw error;
}
}
Retry Logic
Copy
Ask AI
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
delayMs: number = 1000
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) {
throw error;
}
console.log(`Attempt ${i + 1} failed, retrying in ${delayMs}ms...`);
await new Promise(resolve => setTimeout(resolve, delayMs));
delayMs *= 2; // Exponential backoff
}
}
throw new Error('Should not reach here');
}
// Usage
const receipt = await withRetry(() =>
entity.donate(ethers.utils.parseUnits('100', 6))
);
Gas Optimization
Batch Operations
Use multicall pattern for reading multiple entities:Copy
Ask AI
import { ethers } from 'ethers';
interface EntityData {
address: string;
balance: string;
manager: string;
entityType: 'org' | 'fund';
}
async function batchReadEntities(addresses: string[]): Promise<EntityData[]> {
const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
// Batch all calls
const calls = addresses.flatMap(address => {
const entity = new ethers.Contract(address, ENTITY_ABI, provider);
return [
entity.balance(),
entity.manager(),
entity.entityType()
];
});
const results = await Promise.all(calls);
// Parse results
const entities: EntityData[] = [];
for (let i = 0; i < addresses.length; i++) {
const offset = i * 3;
entities.push({
address: addresses[i],
balance: ethers.utils.formatUnits(results[offset], 6),
manager: results[offset + 1],
entityType: results[offset + 2] === 0 ? 'org' : 'fund'
});
}
return entities;
}
Optimize Approvals
Copy
Ask AI
async function optimizedApproval(
tokenAddress: string,
spenderAddress: string,
amount: ethers.BigNumber
): Promise<void> {
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const token = new ethers.Contract(tokenAddress, ERC20_ABI, signer);
// Check current allowance
const currentAllowance = await token.allowance(signer.address, spenderAddress);
// Only approve if needed
if (currentAllowance.gte(amount)) {
console.log('✓ Sufficient allowance already exists');
return;
}
// If there's a non-zero allowance, reset to 0 first (for some tokens like USDT)
if (currentAllowance.gt(0)) {
console.log('Resetting allowance to 0...');
await (await token.approve(spenderAddress, 0)).wait();
}
// Set new allowance
console.log('Setting new allowance...');
await (await token.approve(spenderAddress, amount)).wait();
console.log('✓ Approval complete');
}
Integration Patterns
Pattern 1: Donation Widget
Complete donation flow with user feedback:Copy
Ask AI
interface DonationResult {
success: boolean;
txHash?: string;
error?: string;
amountDonated?: string;
amountReceived?: string;
fee?: string;
}
async function processDonation(
entityAddress: string,
amount: string,
onProgress?: (step: string) => void
): Promise<DonationResult> {
try {
onProgress?.('Validating...');
await safeEntityInteraction(entityAddress);
const amountWei = validateAmount(amount);
onProgress?.('Checking balance...');
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, signer);
const balance = await usdc.balanceOf(signer.address);
if (balance.lt(amountWei)) {
return {
success: false,
error: 'Insufficient USDC balance'
};
}
onProgress?.('Approving USDC...');
await optimizedApproval(USDC_ADDRESS, entityAddress, amountWei);
onProgress?.('Processing donation...');
const entity = new ethers.Contract(entityAddress, ENTITY_ABI, signer);
const tx = await entity.donate(amountWei);
onProgress?.('Confirming transaction...');
const receipt = await tx.wait();
// Parse event for fee info
const event = receipt.events?.find(e => e.event === 'EntityDonationReceived');
return {
success: true,
txHash: receipt.transactionHash,
amountDonated: amount,
amountReceived: event?.args?.amountReceived
? ethers.utils.formatUnits(event.args.amountReceived, 6)
: undefined,
fee: event?.args?.amountFee
? ethers.utils.formatUnits(event.args.amountFee, 6)
: undefined
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
// Usage
const result = await processDonation('0x...', '1000', (step) => {
console.log(`Status: ${step}`);
});
if (result.success) {
console.log(`✓ Donation successful!`);
console.log(` Transaction: ${result.txHash}`);
console.log(` Net to entity: ${result.amountReceived} USDC`);
console.log(` Fee: ${result.fee} USDC`);
} else {
console.log(`✗ Donation failed: ${result.error}`);
}
Pattern 2: Grant Workflow
Structured grant recommendation and execution:Copy
Ask AI
enum GrantStatus {
PENDING = 'pending',
APPROVED = 'approved',
EXECUTED = 'executed',
FAILED = 'failed'
}
interface Grant {
id: string;
fundAddress: string;
orgAddress: string;
orgName: string;
amount: string;
reason: string;
status: GrantStatus;
createdAt: number;
executedAt?: number;
txHash?: string;
error?: string;
}
class GrantWorkflow {
private grants: Map<string, Grant> = new Map();
async createGrant(
fundAddress: string,
orgAddress: string,
orgName: string,
amount: string,
reason: string
): Promise<string> {
const grantId = `grant-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const grant: Grant = {
id: grantId,
fundAddress,
orgAddress,
orgName,
amount,
reason,
status: GrantStatus.PENDING,
createdAt: Date.now()
};
// Validate grant
await this.validateGrant(grant);
this.grants.set(grantId, grant);
console.log(`✓ Grant ${grantId} created`);
return grantId;
}
private async validateGrant(grant: Grant): Promise<void> {
const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
const fund = new ethers.Contract(grant.fundAddress, ENTITY_ABI, provider);
const registry = new ethers.Contract(REGISTRY_ADDRESS, REGISTRY_ABI, provider);
// Check org is active
const isActive = await registry.isActiveEntity(grant.orgAddress);
if (!isActive) {
throw new Error('Recipient organization is not active');
}
// Check fund balance
const balance = await fund.balance();
const amountWei = ethers.utils.parseUnits(grant.amount, 6);
if (balance.lt(amountWei)) {
throw new Error('Insufficient fund balance');
}
}
async approveGrant(grantId: string): Promise<void> {
const grant = this.grants.get(grantId);
if (!grant) throw new Error('Grant not found');
grant.status = GrantStatus.APPROVED;
console.log(`✓ Grant ${grantId} approved`);
}
async executeGrant(grantId: string): Promise<ethers.ContractReceipt> {
const grant = this.grants.get(grantId);
if (!grant) throw new Error('Grant not found');
if (grant.status !== GrantStatus.APPROVED) {
throw new Error('Grant must be approved before execution');
}
try {
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const fund = new ethers.Contract(grant.fundAddress, ENTITY_ABI, signer);
const amountWei = ethers.utils.parseUnits(grant.amount, 6);
console.log(`Executing grant ${grantId}...`);
const tx = await fund.transferToEntity(grant.orgAddress, amountWei);
const receipt = await tx.wait();
grant.status = GrantStatus.EXECUTED;
grant.executedAt = Date.now();
grant.txHash = receipt.transactionHash;
console.log(`✓ Grant ${grantId} executed: ${receipt.transactionHash}`);
return receipt;
} catch (error) {
grant.status = GrantStatus.FAILED;
grant.error = error.message;
console.error(`✗ Grant ${grantId} failed: ${error.message}`);
throw error;
}
}
getGrant(grantId: string): Grant | undefined {
return this.grants.get(grantId);
}
listGrants(status?: GrantStatus): Grant[] {
const grants = Array.from(this.grants.values());
return status ? grants.filter(g => g.status === status) : grants;
}
}
// Usage
const workflow = new GrantWorkflow();
// Create grant recommendation
const grantId = await workflow.createGrant(
'0x...', // fund
'0x...', // org
'Example Charity',
'5000',
'Annual education program support'
);
// Review and approve
await workflow.approveGrant(grantId);
// Execute
await workflow.executeGrant(grantId);
Testing
Local Testing with Hardhat
Copy
Ask AI
import { ethers } from 'hardhat';
import { expect } from 'chai';
describe('Entity Integration Tests', () => {
let entity, usdc, signer;
beforeEach(async () => {
// Fork mainnet
await network.provider.request({
method: 'hardhat_reset',
params: [{
forking: {
jsonRpcUrl: RPC_URL,
blockNumber: 18000000
}
}]
});
[signer] = await ethers.getSigners();
// Get contracts
entity = await ethers.getContractAt('Entity', ENTITY_ADDRESS);
usdc = await ethers.getContractAt('ERC20', USDC_ADDRESS);
// Get USDC from whale
await network.provider.request({
method: 'hardhat_impersonateAccount',
params: [USDC_WHALE]
});
const whale = await ethers.getSigner(USDC_WHALE);
await usdc.connect(whale).transfer(
signer.address,
ethers.utils.parseUnits('10000', 6)
);
});
it('should donate USDC successfully', async () => {
const amount = ethers.utils.parseUnits('100', 6);
await usdc.approve(entity.address, amount);
const balanceBefore = await entity.balance();
await entity.donate(amount);
const balanceAfter = await entity.balance();
expect(balanceAfter.sub(balanceBefore)).to.be.gt(0);
});
it('should handle insufficient balance', async () => {
const amount = ethers.utils.parseUnits('100000', 6); // More than we have
await usdc.approve(entity.address, amount);
await expect(entity.donate(amount)).to.be.reverted;
});
});
Monitoring & Maintenance
Health Check Script
Copy
Ask AI
async function healthCheck(entityAddress: string): Promise<{
healthy: boolean;
issues: string[];
}> {
const issues: string[] = [];
const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
try {
// 1. Check entity is active
const registry = new ethers.Contract(REGISTRY_ADDRESS, REGISTRY_ABI, provider);
const isActive = await registry.isActiveEntity(entityAddress);
if (!isActive) {
issues.push('Entity is not active');
}
// 2. Check balance is reasonable
const entity = new ethers.Contract(entityAddress, ENTITY_ABI, provider);
const balance = await entity.balance();
if (balance.isZero()) {
issues.push('Entity balance is zero');
}
// 3. Check manager is set
const manager = await entity.manager();
if (manager === ethers.constants.AddressZero) {
issues.push('No manager set');
}
// 4. Check contract code hasn't changed
const code = await provider.getCode(entityAddress);
if (code === '0x') {
issues.push('No contract code at address');
}
console.log(`\n🏥 Health Check for ${entityAddress}:`);
if (issues.length === 0) {
console.log('✅ All checks passed');
return { healthy: true, issues: [] };
} else {
console.log('⚠️ Issues found:');
issues.forEach(issue => console.log(` - ${issue}`));
return { healthy: false, issues };
}
} catch (error) {
issues.push(`Health check failed: ${error.message}`);
return { healthy: false, issues };
}
}
// Run health check
await healthCheck('0x...');