This guide covers best practices for developing with the Guardian TypeScript SDK, including security, performance, and maintainability guidelines.
❌ Don't:
// Never hardcode private keys
const privateKey = '0x1234567890abcdef...'✅ Do:
// Use environment variables
const privateKey = process.env.PRIVATE_KEY
if (!privateKey) {
throw new Error('PRIVATE_KEY environment variable is required')
}
// Use secure key management services
import { getSecret } from '@aws-sdk/client-secrets-manager'
const privateKey = await getSecret('guardian-private-key')❌ Don't:
// No validation
const txHash = await secureOwnable.transferOwnershipRequest(newOwner)✅ Do:
// Always validate inputs
function isValidAddress(address: string): boolean {
return /^0x[a-fA-F0-9]{40}$/.test(address)
}
function isValidTimeLock(period: bigint): boolean {
return period > 0n && period <= 86400n // Max 24 hours
}
if (!isValidAddress(newOwner)) {
throw new Error('Invalid address provided')
}
if (!isValidTimeLock(timeLockPeriod)) {
throw new Error('Invalid time lock period')
}
const txHash = await secureOwnable.transferOwnershipRequest(newOwner)❌ Don't:
// Silent failures
try {
await secureOwnable.transferOwnershipRequest(newOwner)
} catch (error) {
// Ignore error
}✅ Do:
// Comprehensive error handling
try {
const txHash = await secureOwnable.transferOwnershipRequest(newOwner)
console.log('Transaction successful:', txHash)
// Wait for confirmation
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash })
if (receipt.status === 'reverted') {
throw new Error('Transaction reverted')
}
} catch (error) {
console.error('Transaction failed:', error.message)
// Handle specific error types
if (error.message.includes('insufficient funds')) {
console.log('Please add more ETH to your account')
} else if (error.message.includes('Only owner')) {
console.log('Only the contract owner can perform this action')
} else if (error.message.includes('Invalid address')) {
console.log('Please provide a valid Ethereum address')
}
// Re-throw if critical
if (error.message.includes('CRITICAL')) {
throw error
}
}❌ Don't:
// No access control checks
const txHash = await secureOwnable.transferOwnershipRequest(newOwner)✅ Do:
// Check permissions before operations
async function transferOwnershipWithChecks(newOwner: string) {
// Check if caller is owner
const currentOwner = await secureOwnable.owner()
if (account.address !== currentOwner) {
throw new Error('Only the contract owner can transfer ownership')
}
// Check if new owner is valid
if (!isValidAddress(newOwner)) {
throw new Error('Invalid new owner address')
}
// Check if new owner is different
if (newOwner.toLowerCase() === currentOwner.toLowerCase()) {
throw new Error('New owner must be different from current owner')
}
return await secureOwnable.transferOwnershipRequest(newOwner)
}❌ Don't:
// Creating new clients for each operation
function createClient() {
return createPublicClient({
chain: mainnet,
transport: http()
})
}
const client1 = createClient()
const client2 = createClient()✅ Do:
// Reuse clients
const publicClient = createPublicClient({
chain: mainnet,
transport: http()
})
const walletClient = createWalletClient({
account: privateKeyToAccount(privateKey),
chain: mainnet,
transport: http()
})
// Use the same clients throughout your application❌ Don't:
// Sequential operations
const result1 = await secureOwnable.updateRecoveryRequestAndApprove(newRecovery1)
const result2 = await secureOwnable.updateRecoveryRequestAndApprove(newRecovery2)
const result3 = await secureOwnable.updateRecoveryRequestAndApprove(newRecovery3)✅ Do:
// Batch operations
const operations = [
secureOwnable.updateRecoveryRequestAndApprove(newRecovery1),
secureOwnable.updateRecoveryRequestAndApprove(newRecovery2),
secureOwnable.updateRecoveryRequestAndApprove(newRecovery3)
]
const results = await Promise.allSettled(operations)
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Operation ${index} successful:`, result.value)
} else {
console.error(`Operation ${index} failed:`, result.reason)
}
})❌ Don't:
// No gas estimation
const txHash = await secureOwnable.transferOwnershipRequest(newOwner)✅ Do:
// Estimate gas and optimize
const gasEstimate = await publicClient.estimateContractGas({
address: contractAddress,
abi: secureOwnable.abi,
functionName: 'transferOwnershipRequest',
args: [newOwner],
account: account.address
})
const txHash = await secureOwnable.transferOwnershipRequest(
newOwner,
{
from: account.address,
gas: gasEstimate * 120n / 100n // Add 20% buffer
}
)❌ Don't:
// No caching
async function getOwner() {
return await secureOwnable.owner() // Always makes RPC call
}✅ Do:
// Implement caching
class CachedSecureOwnable {
private cache = new Map<string, { value: any; timestamp: number }>()
private cacheTimeout = 30000 // 30 seconds
async getOwner(): Promise<Address> {
const cacheKey = 'owner'
const cached = this.cache.get(cacheKey)
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
return cached.value
}
const owner = await this.secureOwnable.owner()
this.cache.set(cacheKey, { value: owner, timestamp: Date.now() })
return owner
}
}❌ Don't:
// Monolithic file
class GuardianManager {
// 1000+ lines of mixed functionality
async doEverything() { /* ... */ }
}✅ Do:
// Modular architecture
class GuardianManager {
constructor(
private secureOwnable: SecureOwnable,
private dynamicRBAC: DynamicRBAC,
) {}
// Delegate to specific modules
async transferOwnership(newOwner: Address) {
return await this.secureOwnable.transferOwnershipRequest(newOwner)
}
async manageRoles(roleHash: string, account: Address) {
return await this.dynamicRBAC.grantRole(roleHash, account)
}
}❌ Don't:
// No type safety
function processContract(address: string) {
// address could be anything
}✅ Do:
// Strong typing
import { Address } from '@guardian/sdk/typescript'
function processContract(address: Address) {
// address is guaranteed to be a valid Ethereum address
}
// Use branded types for additional safety
type ContractAddress = Address & { __brand: 'ContractAddress' }
type UserAddress = Address & { __brand: 'UserAddress' }
function transferOwnership(
contract: ContractAddress,
newOwner: UserAddress
) {
// Type system prevents mixing contract and user addresses
}❌ Don't:
// Hardcoded configuration
const config = {
rpcUrl: 'https://eth-mainnet.alchemyapi.io/v2/hardcoded-key',
contractAddress: '0x1234567890abcdef...'
}✅ Do:
// Environment-based configuration
interface Config {
rpcUrl: string
contractAddress: Address
chainId: number
gasLimit?: bigint
}
function loadConfig(): Config {
return {
rpcUrl: process.env.RPC_URL || 'http://127.0.0.1:8545',
contractAddress: process.env.CONTRACT_ADDRESS as Address,
chainId: parseInt(process.env.CHAIN_ID || '1'),
gasLimit: process.env.GAS_LIMIT ? BigInt(process.env.GAS_LIMIT) : undefined
}
}
// Validate configuration
function validateConfig(config: Config): void {
if (!config.rpcUrl) throw new Error('RPC_URL is required')
if (!config.contractAddress) throw new Error('CONTRACT_ADDRESS is required')
if (!config.chainId) throw new Error('CHAIN_ID is required')
}❌ Don't:
// Unclear test structure
it('should work', async () => {
// Mixed setup, execution, and assertions
const client = createPublicClient({...})
const secureOwnable = new SecureOwnable(client, ...)
const result = await secureOwnable.owner()
expect(result).toBe('0x...')
})✅ Do:
// Clear test structure
describe('SecureOwnable', () => {
let client: PublicClient
let secureOwnable: SecureOwnable
let contractAddress: Address
beforeEach(() => {
// Setup
client = createPublicClient({...})
contractAddress = '0x...' as Address
secureOwnable = new SecureOwnable(client, undefined, contractAddress, mainnet)
})
describe('owner()', () => {
it('should return the contract owner', async () => {
// Given
const expectedOwner = '0x...' as Address
// When
const actualOwner = await secureOwnable.owner()
// Then
expect(actualOwner).toBe(expectedOwner)
})
})
})❌ Don't:
// No mocking, always hits real network
it('should transfer ownership', async () => {
const txHash = await secureOwnable.transferOwnershipRequest('0x...')
expect(txHash).toBeDefined()
})✅ Do:
// Mock external dependencies
import { vi } from 'vitest'
describe('SecureOwnable', () => {
let mockClient: PublicClient
beforeEach(() => {
mockClient = {
readContract: vi.fn(),
writeContract: vi.fn(),
waitForTransactionReceipt: vi.fn()
} as any
})
it('should transfer ownership', async () => {
// Given
const expectedTxHash = '0x123...' as Hash
vi.mocked(mockClient.writeContract).mockResolvedValue(expectedTxHash)
// When
const txHash = await secureOwnable.transferOwnershipRequest('0x...')
// Then
expect(txHash).toBe(expectedTxHash)
expect(mockClient.writeContract).toHaveBeenCalledWith({
address: contractAddress,
abi: expect.any(Array),
functionName: 'transferOwnershipRequest',
args: ['0x...']
})
})
})❌ Don't:
// No integration tests
// Only unit tests with mocks✅ Do:
// Include integration tests
describe('Integration Tests', () => {
it('should complete ownership transfer workflow', async () => {
// Use real contracts on testnet
const testnetClient = createPublicClient({
chain: goerli,
transport: http()
})
const secureOwnable = new SecureOwnable(
testnetClient,
walletClient,
testnetContractAddress,
goerli
)
// Test complete workflow
const requestTx = await secureOwnable.transferOwnershipRequest(newOwner)
expect(requestTx).toBeDefined()
// Wait for time lock
await new Promise(resolve => setTimeout(resolve, timeLockPeriod * 1000))
const approveTx = await secureOwnable.transferOwnershipDelayedApproval(txId)
expect(approveTx).toBeDefined()
// Verify final state
const finalOwner = await secureOwnable.owner()
expect(finalOwner.toLowerCase()).toBe(newOwner.toLowerCase())
})
})❌ Don't:
// Unstructured logging
console.log('Transaction successful')
console.log('Error occurred')✅ Do:
// Structured logging
import { createLogger } from 'winston'
const logger = createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'guardian.log' })
]
})
// Use structured logging
logger.info('Transaction successful', {
txHash,
contractAddress,
functionName: 'transferOwnershipRequest',
newOwner,
timestamp: Date.now()
})
logger.error('Transaction failed', {
error: error.message,
contractAddress,
functionName: 'transferOwnershipRequest',
newOwner,
timestamp: Date.now()
})❌ Don't:
// No metrics
const txHash = await secureOwnable.transferOwnershipRequest(newOwner)✅ Do:
// Collect metrics
class MetricsCollector {
private metrics = {
transactions: 0,
errors: 0,
gasUsed: 0n,
responseTime: 0
}
async trackTransaction<T>(
operation: () => Promise<T>,
operationName: string
): Promise<T> {
const startTime = Date.now()
try {
const result = await operation()
this.metrics.transactions++
this.metrics.responseTime = Date.now() - startTime
logger.info('Transaction completed', {
operation: operationName,
duration: this.metrics.responseTime,
success: true
})
return result
} catch (error) {
this.metrics.errors++
this.metrics.responseTime = Date.now() - startTime
logger.error('Transaction failed', {
operation: operationName,
duration: this.metrics.responseTime,
error: error.message
})
throw error
}
}
}❌ Don't:
// Same configuration for all environments
const config = {
rpcUrl: 'https://eth-mainnet.alchemyapi.io/v2/production-key',
contractAddress: '0x...'
}✅ Do:
// Environment-specific configuration
const environments = {
development: {
rpcUrl: 'http://127.0.0.1:8545',
contractAddress: '0x...' as Address,
chainId: 1337
},
testnet: {
rpcUrl: 'https://eth-goerli.alchemyapi.io/v2/test-key',
contractAddress: '0x...' as Address,
chainId: 5
},
production: {
rpcUrl: 'https://eth-mainnet.alchemyapi.io/v2/prod-key',
contractAddress: '0x...' as Address,
chainId: 1
}
}
const config = environments[process.env.NODE_ENV || 'development']❌ Don't:
// No health monitoring
app.listen(3000)✅ Do:
// Implement health checks
app.get('/health', async (req, res) => {
try {
// Check RPC connection
const blockNumber = await publicClient.getBlockNumber()
// Check contract accessibility
const owner = await secureOwnable.owner()
res.json({
status: 'healthy',
blockNumber: blockNumber.toString(),
contractOwner: owner,
timestamp: Date.now()
})
} catch (error) {
res.status(500).json({
status: 'unhealthy',
error: error.message,
timestamp: Date.now()
})
}
})❌ Don't:
// No documentation
function transferOwnership(newOwner: string) {
return secureOwnable.transferOwnershipRequest(newOwner)
}✅ Do:
/**
* Transfers ownership of the contract to a new owner
*
* @param newOwner - The address of the new owner
* @param options - Optional transaction parameters
* @returns Promise resolving to the transaction hash
*
* @throws {Error} If the caller is not the current owner
* @throws {Error} If the new owner address is invalid
* @throws {Error} If the transaction fails
*
* @example
* ```typescript
* const txHash = await transferOwnership('0x...')
* console.log('Ownership transfer requested:', txHash)
* ```
*/
async function transferOwnership(
newOwner: Address,
options?: TransactionOptions
): Promise<Hash> {
// Implementation
}❌ Don't:
# My Project
This is my project.✅ Do:
# Guardian TypeScript SDK Integration
A comprehensive integration with Guardian protocol contracts.
## Features
- ✅ SecureOwnable contract integration
- ✅ DynamicRBAC role management
- ✅ Workflow analysis and validation
- ✅ Protocol compliance checking
## Quick Start
```typescript
import { SecureOwnable } from '@guardian/sdk/typescript'
const secureOwnable = new SecureOwnable(client, walletClient, address, chain)
const owner = await secureOwnable.owner()See API Documentation for complete reference.
See Contributing Guide for development guidelines.
---
**Ready to implement these practices?** Check out the [Examples](./examples-basic.md) for practical implementations.