📘 Node.js Guide - QubiSafeGuardian

Complete guide for Node.js integrating with QubiSafeGuardian Smart Contract


🏗️ Smart Contract Architecture {#architecture}

High-Level Overview

┌─────────────────────────────────────────────────────────┐
│                QubiSafeGuardian                       │
│                 (UUPS Proxy Pattern)                    │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐ │
│  │ Access       │  │  Multi-Sig   │  │   Timelock   │ │
│  │ Control      │  │  Validation  │  │   System     │ │
│  └──────────────┘  └──────────────┘  └──────────────┘ │
│                                                         │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐ │
│  │ Daily Limit  │  │   Pausable   │  │  Reentrancy  │ │
│  │ Tracking     │  │   Controls   │  │     Guard    │ │
│  └──────────────┘  └──────────────┘  └──────────────┘ │
│                                                         │
├─────────────────────────────────────────────────────────┤
│              Withdrawal Processing Flow                │
│                                                         │
│  1. submitWithdrawal()                                  │
│     ├─ Verify signatures                               │
│     ├─ Check daily limits                              │
│     ├─ Amount < instantThreshold?                      │
│     │   ├─ YES: Execute immediately                    │
│     │   └─ NO: Queue for timelock                      │
│     └─ Emit events                                     │
│                                                         │
│  2. executeQueuedWithdrawal() [Operator only]          │
│     ├─ Check timelock expired                          │
│     ├─ Verify request still valid                      │
│     └─ Execute transfer                                │
│                                                         │
│  3. cancelQueuedRequest() [Admin/User]                 │
│     └─ Cancel before execution                         │
│                                                         │
└─────────────────────────────────────────────────────────┘

Contract Inheritance Chain

QubiSafeGuardian
  ├─ Initializable           // Proxy initialization
  ├─ AccessControlUpgradeable // Role-based permissions
  ├─ PausableUpgradeable      // Emergency pause
  ├─ ReentrancyGuardUpgradeable // Attack prevention
  ├─ UUPSUpgradeable          // Upgradeable pattern
  └─ EIP712Upgradeable        // Signature standard

Key Design Principles

  1. Defense in Depth: Multiple security layers
  2. Fail-Safe Defaults: Secure by default
  3. Separation of Concerns: Clear responsibilities
  4. Upgradeability: Can fix bugs without redeployment
  5. Gas Efficiency: Optimized for cost

📋 All Functions Overview {#functions-overview}

Configuration Functions (Admin Only)

Core Withdrawal Functions

Security & Emergency Functions (Admin Only)

Access Control Functions

View Functions (Read-Only - Anyone Can Call)

Upgradeability Function


📊 State Variables Deep Dive {#state-variables}

Configuration Variables

1. instantWithdrawalThreshold

uint256 public instantWithdrawalThreshold; // e.g., 1000 ETH

Purpose: Determines when withdrawals need timelock
Default: 1000 ETH (1000 * 10^18 wei)
Logic:

Node.js Usage:

// Read threshold
const threshold = await treasury.instantWithdrawalThreshold();
console.log('Instant threshold:', ethers.formatEther(threshold), 'ETH');

// Update threshold (admin only)
const tx = await treasury.updateTreasurySettings(
  ethers.parseEther("2000"), // New threshold: 2000 ETH
  perUserLimit,               // Keep existing
  globalLimit,                // Keep existing
  timelockDuration,           // Keep existing
  requiredSigners             // Keep existing
);
await tx.wait();

2. perUserDailyLimit

uint256 public perUserDailyLimit; // e.g., 5000 ETH

Purpose: Maximum amount a single user can withdraw per day
Default: 5000 ETH
Resets: Every 24 hours (rolling window)

Node.js Usage:

// Check user's remaining limit
const userAddress = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb";
const dailyUsage = await treasury.userDailyWithdrawals(userAddress);
const limit = await treasury.perUserDailyLimit();
const remaining = limit - dailyUsage.amount;

console.log('Daily limit:', ethers.formatEther(limit), 'ETH');
console.log('Used today:', ethers.formatEther(dailyUsage.amount), 'ETH');
console.log('Remaining:', ethers.formatEther(remaining), 'ETH');
console.log('Resets at:', new Date(Number(dailyUsage.lastResetTime) * 1000 + 86400000));

3. globalDailyLimit

uint256 public globalDailyLimit; // e.g., 50000 ETH

Purpose: Maximum total withdrawals across all users per day
Default: 50000 ETH
Use Case: Prevents mass exodus in case of compromise

Node.js Usage:

// Check global usage
const globalUsage = await treasury.globalDailyWithdrawals();
const globalLimit = await treasury.globalDailyLimit();
const globalRemaining = globalLimit - globalUsage.amount;

console.log('Global limit:', ethers.formatEther(globalLimit), 'ETH');
console.log('Used today:', ethers.formatEther(globalUsage.amount), 'ETH');
console.log('Remaining:', ethers.formatEther(globalRemaining), 'ETH');

4. timelockDuration

uint256 public timelockDuration; // e.g., 86400 seconds (24 hours)

Purpose: How long large withdrawals must wait
Default: 86400 seconds (24 hours)
Security: Gives time to detect and cancel malicious requests

Node.js Usage:

// Read duration
const duration = await treasury.timelockDuration();
console.log('Timelock duration:', duration / 3600, 'hours');

// Calculate execution time for a queued request
const request = await treasury.queuedRequests(requestId);
const canExecuteAt = Number(request.queuedAt) + Number(duration);
const now = Math.floor(Date.now() / 1000);
const remainingTime = canExecuteAt - now;

console.log('Can execute at:', new Date(canExecuteAt * 1000));
console.log('Time remaining:', remainingTime / 3600, 'hours');

5. requiredSigners

uint256 public requiredSigners; // e.g., 2

Purpose: How many signatures needed for withdrawals
Default: 2
Range: 1 to total number of signers

Node.js Usage:

// Check required signers
const required = await treasury.requiredSigners();
console.log('Required signatures:', required);

// Get all signers
const signerAddresses = []; // You need to track this off-chain
for (const addr of potentialSigners) {
  const isSigner = await treasury.authorizedSigners(addr);
  if (isSigner) {
    signerAddresses.push(addr);
  }
}
console.log('Total signers:', signerAddresses.length);
console.log('Required:', required, 'of', signerAddresses.length);

Mapping Variables

6. authorizedSigners

mapping(address => bool) public authorizedSigners;

Purpose: Tracks who can sign withdrawal requests
Access: Public read, admin write

Node.js Usage:

// Check if address is a signer
const address = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb";
const isSigner = await treasury.authorizedSigners(address);
console.log(address, 'is signer:', isSigner);

// Add a signer (admin only)
const addTx = await treasury.updateSigner(address, true);
await addTx.wait();
console.log('Signer added');

// Remove a signer (admin only)
const removeTx = await treasury.updateSigner(address, false);
await removeTx.wait();
console.log('Signer removed');

7. tokenAllowlist

mapping(address => bool) public tokenAllowlist;

Purpose: Which ERC20 tokens can be withdrawn
Special: Native ETH (address(0)) is always allowed

Node.js Usage:

// Check if token is allowed
const USDT = "0x55d398326f99059fF775485246999027B3197955"; // BSC USDT
const isAllowed = await treasury.tokenAllowlist(USDT);
console.log('USDT allowed:', isAllowed);

// Add token to allowlist (admin only)
const tx = await treasury.updateTokenAllowlist(USDT, true);
await tx.wait();
console.log('Token added to allowlist');

// For native ETH, use zero address
const ETH_ADDRESS = ethers.ZeroAddress;
// ETH is always allowed, no need to add

8. usedRequestIds

mapping(bytes32 => bool) public usedRequestIds;

Purpose: Prevents replay attacks (same request executed twice)
How it works: Each requestId can only be used once

Node.js Usage:

// Generate unique request ID
const requestId = ethers.keccak256(
  ethers.AbiCoder.defaultAbiCoder().encode(
    ['address', 'uint256', 'uint256'],
    [recipient, amount, Date.now()]
  )
);

// Check if already used
const isUsed = await treasury.usedRequestIds(requestId);
if (isUsed) {
  console.error('This request ID was already used!');
  // Generate a new one
}

9. userDailyWithdrawals

mapping(address => DailyLimit) public userDailyWithdrawals;

struct DailyLimit {
    uint256 amount;
    uint256 lastResetTime;
}

Purpose: Tracks each user’s daily withdrawal amount
Auto-reset: After 24 hours

Node.js Usage:

// Get user's daily stats
const userStats = await treasury.userDailyWithdrawals(userAddress);
console.log('Amount withdrawn today:', ethers.formatEther(userStats.amount), 'ETH');
console.log('Last reset:', new Date(Number(userStats.lastResetTime) * 1000));

// Check if limit will reset soon
const now = Math.floor(Date.now() / 1000);
const timeSinceReset = now - Number(userStats.lastResetTime);
const willResetIn = 86400 - timeSinceReset; // seconds until reset

if (willResetIn < 3600) {
  console.log('Limit resets in', Math.floor(willResetIn / 60), 'minutes');
}

10. globalDailyWithdrawals

DailyLimit public globalDailyWithdrawals;

Purpose: Tracks total withdrawals across all users
Same structure: Uses DailyLimit struct

Node.js Usage:

// Get global stats
const globalStats = await treasury.globalDailyWithdrawals();
const globalLimit = await treasury.globalDailyLimit();
const percentUsed = (Number(globalStats.amount) * 100) / Number(globalLimit);

console.log('Global usage:', percentUsed.toFixed(2), '%');
console.log('Amount:', ethers.formatEther(globalStats.amount), 'ETH');
console.log('Limit:', ethers.formatEther(globalLimit), 'ETH');

if (percentUsed > 80) {
  console.warn('⚠️  Global limit almost reached!');
}

11. queuedRequests

mapping(bytes32 => WithdrawalRequest) public queuedRequests;

struct WithdrawalRequest {
    address recipient;
    address tokenAddress;
    uint256 amount;
    uint256 queuedAt;
    bool executed;
    bool cancelled;
}

Purpose: Stores large withdrawal requests waiting for timelock
Lifecycle: Created → Queued → Executed or Cancelled

Node.js Usage:

// Get queued request details
const requestId = "0x1234..."; // From event
const request = await treasury.queuedRequests(requestId);

console.log('Request Details:');
console.log('├─ Recipient:', request.recipient);
console.log('├─ Token:', request.tokenAddress === ethers.ZeroAddress ? 'ETH' : request.tokenAddress);
console.log('├─ Amount:', ethers.formatEther(request.amount));
console.log('├─ Queued at:', new Date(Number(request.queuedAt) * 1000));
console.log('├─ Executed:', request.executed);
console.log('└─ Cancelled:', request.cancelled);

// Check if ready to execute
const timelockDuration = await treasury.timelockDuration();
const canExecuteAt = Number(request.queuedAt) + Number(timelockDuration);
const now = Math.floor(Date.now() / 1000);

if (now >= canExecuteAt && !request.executed && !request.cancelled) {
  console.log('✅ Ready to execute!');
} else if (request.executed) {
  console.log('✅ Already executed');
} else if (request.cancelled) {
  console.log('❌ Cancelled');
} else {
  console.log('⏳ Waiting...', Math.floor((canExecuteAt - now) / 60), 'minutes remaining');
}

Role Constants

12. DEFAULT_ADMIN_ROLE & OPERATOR_ROLE

bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");

Purpose: Access control identifiers
DEFAULT_ADMIN_ROLE: Can do everything (configure, pause, emergency withdraw)
OPERATOR_ROLE: Can execute queued withdrawals

Node.js Usage:

// Get role constants
const ADMIN_ROLE = await treasury.DEFAULT_ADMIN_ROLE();
const OPERATOR_ROLE = await treasury.OPERATOR_ROLE();

// Check if address has role
const hasAdmin = await treasury.hasRole(ADMIN_ROLE, address);
const hasOperator = await treasury.hasRole(OPERATOR_ROLE, address);

console.log(address);
console.log('├─ Is Admin:', hasAdmin);
console.log('└─ Is Operator:', hasOperator);

// Grant role (admin only)
const grantTx = await treasury.grantRole(OPERATOR_ROLE, operatorAddress);
await grantTx.wait();
console.log('Operator role granted');

// Revoke role (admin only)
const revokeTx = await treasury.revokeRole(OPERATOR_ROLE, operatorAddress);
await revokeTx.wait();
console.log('Operator role revoked');

🔧 Functions Reference {#functions-reference}

Initialization Functions

initialize()

function initialize() external initializer

Purpose: Sets up the contract on first deployment
Called by: Deployment script automatically
Can only be called: Once (initializer modifier)

What it does:

  1. Initializes all parent contracts (AccessControl, Pausable, etc.)
  2. Sets default configuration:
  3. Grants DEFAULT_ADMIN_ROLE to deployer
  4. Sets up EIP-712 domain

Node.js Usage:

// This is handled by deployment script
const Treasury = await ethers.getContractFactory("QubiSafeGuardian");
const treasury = await upgrades.deployProxy(
  Treasury,
  [], // initialize() has no parameters
  { kind: 'uups' }
);
await treasury.waitForDeployment();
// initialize() is called automatically

Configuration Functions

updateTreasurySettings()

function updateTreasurySettings(
    uint256 _instantWithdrawalThreshold,
    uint256 _perUserDailyLimit,
    uint256 _globalDailyLimit,
    uint256 _timelockDuration,
    uint256 _requiredSigners
) external onlyRole(DEFAULT_ADMIN_ROLE)

Purpose: Update all treasury configuration
Access: Admin only
Validates:

Node.js Example:

// Read current settings
const currentThreshold = await treasury.instantWithdrawalThreshold();
const currentUserLimit = await treasury.perUserDailyLimit();
const currentGlobalLimit = await treasury.globalDailyLimit();
const currentTimelock = await treasury.timelockDuration();
const currentSigners = await treasury.requiredSigners();

console.log('Current Settings:');
console.log('├─ Instant threshold:', ethers.formatEther(currentThreshold), 'ETH');
console.log('├─ Per-user daily:', ethers.formatEther(currentUserLimit), 'ETH');
console.log('├─ Global daily:', ethers.formatEther(currentGlobalLimit), 'ETH');
console.log('├─ Timelock:', currentTimelock / 3600, 'hours');
console.log('└─ Required signers:', currentSigners);

// Update settings (admin only)
const tx = await treasury.updateTreasurySettings(
  ethers.parseEther("2000"),  // New instant threshold: 2000 ETH
  ethers.parseEther("10000"), // New user daily: 10000 ETH
  ethers.parseEther("100000"),// New global daily: 100000 ETH
  48 * 3600,                  // New timelock: 48 hours
  3                           // New required signers: 3
);

const receipt = await tx.wait();
console.log('Settings updated! Tx:', receipt.hash);

// Listen for event
treasury.on('TreasurySettingsUpdated', 
  (threshold, userLimit, globalLimit, timelock, signers) => {
    console.log('Settings changed:');
    console.log('├─ Threshold:', ethers.formatEther(threshold), 'ETH');
    console.log('├─ User limit:', ethers.formatEther(userLimit), 'ETH');
    console.log('├─ Global limit:', ethers.formatEther(globalLimit), 'ETH');
    console.log('├─ Timelock:', timelock / 3600, 'hours');
    console.log('└─ Signers:', signers);
  }
);

updateSigner()

function updateSigner(address signer, bool status) 
    external 
    onlyRole(DEFAULT_ADMIN_ROLE)

Purpose: Add or remove authorized signers
Access: Admin only
Parameters:

Node.js Example:

// Add a signer
const newSigner = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb";
const addTx = await treasury.updateSigner(newSigner, true);
await addTx.wait();
console.log('✅ Signer added:', newSigner);

// Remove a signer
const oldSigner = "0x1234...";
const removeTx = await treasury.updateSigner(oldSigner, false);
await removeTx.wait();
console.log('❌ Signer removed:', oldSigner);

// Check signer status
const isSigner = await treasury.authorizedSigners(newSigner);
console.log('Is authorized signer:', isSigner);

// Listen for events
treasury.on('SignerUpdated', (signer, status) => {
  console.log(`Signer ${signer} ${status ? 'added' : 'removed'}`);
});

// Best practice: Track all signers
const allSigners = [];
const filter = treasury.filters.SignerUpdated();
const events = await treasury.queryFilter(filter);
for (const event of events) {
  const { signer, status } = event.args;
  if (status) {
    if (!allSigners.includes(signer)) allSigners.push(signer);
  } else {
    const index = allSigners.indexOf(signer);
    if (index > -1) allSigners.splice(index, 1);
  }
}
console.log('All current signers:', allSigners);

updateTokenAllowlist()

function updateTokenAllowlist(address tokenAddress, bool status) 
    external 
    onlyRole(DEFAULT_ADMIN_ROLE)

Purpose: Add or remove ERC20 tokens from allowlist
Access: Admin only
Note: Native ETH (address(0)) is always allowed

Node.js Example:

// Common BSC tokens
const tokens = {
  USDT: "0x55d398326f99059fF775485246999027B3197955",
  USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
  BUSD: "0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56",
  DAI: "0x1AF3F329e8BE154074D8769D1FFa4eE058B1DBc3"
};

// Add USDT to allowlist
const addTx = await treasury.updateTokenAllowlist(tokens.USDT, true);
await addTx.wait();
console.log('✅ USDT allowed');

// Add multiple tokens
for (const [name, address] of Object.entries(tokens)) {
  const tx = await treasury.updateTokenAllowlist(address, true);
  await tx.wait();
  console.log(`✅ ${name} allowed`);
}

// Check if token is allowed
const isUSDTAllowed = await treasury.tokenAllowlist(tokens.USDT);
console.log('USDT allowed:', isUSDTAllowed);

// Remove token
const removeTx = await treasury.updateTokenAllowlist(tokens.BUSD, false);
await removeTx.wait();
console.log('❌ BUSD removed from allowlist');

// Get token info using ethers
const ERC20_ABI = [
  "function name() view returns (string)",
  "function symbol() view returns (string)",
  "function decimals() view returns (uint8)",
  "function balanceOf(address) view returns (uint256)"
];
const tokenContract = new ethers.Contract(tokens.USDT, ERC20_ABI, provider);
const name = await tokenContract.name();
const symbol = await tokenContract.symbol();
const decimals = await tokenContract.decimals();
console.log(`Token: ${name} (${symbol}), Decimals: ${decimals}`);

Core Withdrawal Functions

submitWithdrawal()

function submitWithdrawal(
    bytes32 requestId,
    address recipient,
    address tokenAddress,
    uint256 amount,
    uint256 expiry,
    bytes[] calldata signatures
) external nonReentrant whenNotPaused

Purpose: Main function to request a withdrawal
Access: Anyone (but needs valid signatures)
Flow:

  1. Validates request (not used, not expired, valid signatures)
  2. Checks daily limits
  3. If amount ≤ threshold: Execute immediately
  4. If amount > threshold: Queue for timelock

Parameters Explained:

Node.js Example - Instant Withdrawal (ETH):

const ethers = require('ethers');

// Setup
const provider = new ethers.JsonRpcProvider('https://data-seed-prebsc-1-s1.binance.org:8545/');
const adminWallet = new ethers.Wallet(ADMIN_PRIVATE_KEY, provider);
const signer1Wallet = new ethers.Wallet(SIGNER1_PRIVATE_KEY, provider);
const signer2Wallet = new ethers.Wallet(SIGNER2_PRIVATE_KEY, provider);

const treasury = new ethers.Contract(TREASURY_ADDRESS, TREASURY_ABI, adminWallet);

// Step 1: Create withdrawal request
const recipient = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb";
const amount = ethers.parseEther("500"); // 500 ETH (below 1000 threshold)
const tokenAddress = ethers.ZeroAddress; // ETH
const expiry = Math.floor(Date.now() / 1000) + 3600; // Valid for 1 hour

// Generate unique request ID
const requestId = ethers.keccak256(
  ethers.AbiCoder.defaultAbiCoder().encode(
    ['address', 'address', 'uint256', 'uint256', 'uint256'],
    [recipient, tokenAddress, amount, expiry, Date.now()]
  )
);

console.log('Request ID:', requestId);

// Step 2: Create EIP-712 typed data
const domain = {
  name: 'QubiSafeGuardian',
  version: '2.0',
  chainId: 97, // BSC Testnet
  verifyingContract: TREASURY_ADDRESS
};

const types = {
  WithdrawalRequest: [
    { name: 'requestId', type: 'bytes32' },
    { name: 'recipient', type: 'address' },
    { name: 'tokenAddress', type: 'address' },
    { name: 'amount', type: 'uint256' },
    { name: 'expiry', type: 'uint256' }
  ]
};

const value = {
  requestId,
  recipient,
  tokenAddress,
  amount,
  expiry
};

// Step 3: Get signatures from authorized signers
const signature1 = await signer1Wallet.signTypedData(domain, types, value);
const signature2 = await signer2Wallet.signTypedData(domain, types, value);
const signatures = [signature1, signature2];

console.log('Signatures collected');

// Step 4: Submit withdrawal
try {
  const tx = await treasury.submitWithdrawal(
    requestId,
    recipient,
    tokenAddress,
    amount,
    expiry,
    signatures
  );
  
  console.log('Transaction sent:', tx.hash);
  const receipt = await tx.wait();
  console.log('✅ Withdrawal executed instantly!');
  console.log('Gas used:', receipt.gasUsed.toString());
  
  // Check for events
  const events = receipt.logs
    .map(log => {
      try {
        return treasury.interface.parseLog(log);
      } catch {
        return null;
      }
    })
    .filter(event => event !== null);
    
  for (const event of events) {
    if (event.name === 'WithdrawalExecuted') {
      console.log('Event: WithdrawalExecuted');
      console.log('├─ Request ID:', event.args.requestId);
      console.log('├─ Recipient:', event.args.recipient);
      console.log('├─ Amount:', ethers.formatEther(event.args.amount), 'ETH');
      console.log('└─ Instant:', event.args.instantExecution);
    }
  }
  
} catch (error) {
  console.error('❌ Withdrawal failed:', error.message);
  
  // Parse custom errors
  if (error.data) {
    try {
      const decodedError = treasury.interface.parseError(error.data);
      console.error('Error:', decodedError.name);
      if (decodedError.name === 'InsufficientSigners') {
        console.error('Need more signatures. Provided:', decodedError.args[0], 'Required:', decodedError.args[1]);
      } else if (decodedError.name === 'PerUserDailyLimitExceeded') {
        console.error('User daily limit exceeded');
        console.error('Current:', ethers.formatEther(decodedError.args[0]), 'ETH');
        console.error('Attempted:', ethers.formatEther(decodedError.args[1]), 'ETH');
        console.error('Limit:', ethers.formatEther(decodedError.args[2]), 'ETH');
      }
    } catch {
      console.error('Unknown error');
    }
  }
}

Node.js Example - Queued Withdrawal (Large Amount):

// Same setup as above...

// Create large withdrawal (>1000 ETH)
const largeAmount = ethers.parseEther("5000"); // 5000 ETH (above threshold)

// Generate request ID
const requestId = ethers.keccak256(
  ethers.AbiCoder.defaultAbiCoder().encode(
    ['address', 'address', 'uint256', 'uint256', 'uint256'],
    [recipient, tokenAddress, largeAmount, expiry, Date.now()]
  )
);

// Get signatures (same process)
const value = {
  requestId,
  recipient,
  tokenAddress,
  amount: largeAmount,
  expiry
};

const signature1 = await signer1Wallet.signTypedData(domain, types, value);
const signature2 = await signer2Wallet.signTypedData(domain, types, value);

// Submit
const tx = await treasury.submitWithdrawal(
  requestId,
  recipient,
  tokenAddress,
  largeAmount,
  expiry,
  [signature1, signature2]
);

const receipt = await tx.wait();
console.log('✅ Withdrawal queued for timelock');

// Check for WithdrawalQueued event
const queuedEvent = receipt.logs
  .map(log => {
    try { return treasury.interface.parseLog(log); } catch { return null; }
  })
  .find(e => e && e.name === 'WithdrawalQueued');

if (queuedEvent) {
  const queuedAt = Number(queuedEvent.args.queuedAt);
  const timelockDuration = await treasury.timelockDuration();
  const executeAt = queuedAt + Number(timelockDuration);
  
  console.log('Queued at:', new Date(queuedAt * 1000));
  console.log('Can execute at:', new Date(executeAt * 1000));
  console.log('Wait time:', Number(timelockDuration) / 3600, 'hours');
}

// Save request ID for later execution
console.log('Save this request ID:', requestId);

Node.js Example - ERC20 Withdrawal:

// Withdraw USDT
const USDT_ADDRESS = "0x55d398326f99059fF775485246999027B3197955";
const usdtAmount = ethers.parseUnits("10000", 18); // 10,000 USDT (18 decimals on BSC)

// First, check if token is in allowlist
const isAllowed = await treasury.tokenAllowlist(USDT_ADDRESS);
if (!isAllowed) {
  console.error('❌ Token not in allowlist');
  // Admin needs to add it first
  const addTx = await treasury.connect(adminWallet).updateTokenAllowlist(USDT_ADDRESS, true);
  await addTx.wait();
  console.log('✅ Token added to allowlist');
}

// Check treasury balance
const tokenContract = new ethers.Contract(
  USDT_ADDRESS,
  ['function balanceOf(address) view returns (uint256)'],
  provider
);
const treasuryBalance = await tokenContract.balanceOf(TREASURY_ADDRESS);
console.log('Treasury USDT balance:', ethers.formatUnits(treasuryBalance, 18));

if (treasuryBalance < usdtAmount) {
  console.error('❌ Insufficient treasury balance');
  return;
}

// Create withdrawal request
const requestId = ethers.keccak256(
  ethers.AbiCoder.defaultAbiCoder().encode(
    ['address', 'address', 'uint256', 'uint256', 'uint256'],
    [recipient, USDT_ADDRESS, usdtAmount, expiry, Date.now()]
  )
);

// Get signatures
const value = {
  requestId,
  recipient,
  tokenAddress: USDT_ADDRESS,
  amount: usdtAmount,
  expiry
};

const sig1 = await signer1Wallet.signTypedData(domain, types, value);
const sig2 = await signer2Wallet.signTypedData(domain, types, value);

// Submit
const tx = await treasury.submitWithdrawal(
  requestId,
  recipient,
  USDT_ADDRESS,
  usdtAmount,
  expiry,
  [sig1, sig2]
);

await tx.wait();
console.log('✅ USDT withdrawal completed');

executeQueuedWithdrawal()

function executeQueuedWithdrawal(bytes32 requestId) 
    external 
    onlyRole(OPERATOR_ROLE) 
    nonReentrant 
    whenNotPaused

Purpose: Execute a withdrawal after timelock expires
Access: Operator role only
Validates:

Node.js Example:

// Step 1: Get the request ID (from previous withdrawal)
const requestId = "0x1234..."; // From WithdrawalQueued event

// Step 2: Check request details
const request = await treasury.queuedRequests(requestId);
console.log('Request Details:');
console.log('├─ Recipient:', request.recipient);
console.log('├─ Token:', request.tokenAddress);
console.log('├─ Amount:', ethers.formatEther(request.amount), 'ETH');
console.log('├─ Queued at:', new Date(Number(request.queuedAt) * 1000));
console.log('├─ Executed:', request.executed);
console.log('└─ Cancelled:', request.cancelled);

// Step 3: Check if timelock has expired
const timelockDuration = await treasury.timelockDuration();
const canExecuteAt = Number(request.queuedAt) + Number(timelockDuration);
const now = Math.floor(Date.now() / 1000);

if (now < canExecuteAt) {
  const remainingSeconds = canExecuteAt - now;
  console.log('⏳ Timelock not expired yet');
  console.log('Can execute in:', Math.floor(remainingSeconds / 60), 'minutes');
  return;
}

if (request.executed) {
  console.log('✅ Already executed');
  return;
}

if (request.cancelled) {
  console.log('❌ Request was cancelled');
  return;
}

// Step 4: Execute (operator only)
const operatorWallet = new ethers.Wallet(OPERATOR_PRIVATE_KEY, provider);
const treasuryAsOperator = treasury.connect(operatorWallet);

try {
  const tx = await treasuryAsOperator.executeQueuedWithdrawal(requestId);
  console.log('Transaction sent:', tx.hash);
  
  const receipt = await tx.wait();
  console.log('✅ Queued withdrawal executed!');
  console.log('Gas used:', receipt.gasUsed.toString());
  
  // Parse event
  const executedEvent = receipt.logs
    .map(log => {
      try { return treasury.interface.parseLog(log); } catch { return null; }
    })
    .find(e => e && e.name === 'WithdrawalExecuted');
    
  if (executedEvent) {
    console.log('Funds transferred:');
    console.log('├─ To:', executedEvent.args.recipient);
    console.log('├─ Amount:', ethers.formatEther(executedEvent.args.amount));
    console.log('└─ Instant:', executedEvent.args.instantExecution); // false
  }
  
} catch (error) {
  console.error('❌ Execution failed:', error.message);
  
  if (error.data) {
    try {
      const decodedError = treasury.interface.parseError(error.data);
      if (decodedError.name === 'TimelockNotExpired') {
        console.error('Timelock period not yet passed');
      } else if (decodedError.name === 'RequestAlreadyExecuted') {
        console.error('Request was already executed');
      } else if (decodedError.name === 'RequestCancelled') {
        console.error('Request was cancelled');
      } else if (decodedError.name === 'InsufficientContractBalance') {
        console.error('Treasury has insufficient balance');
      }
    } catch {
      console.error('Unknown error');
    }
  }
}

Automated Executor Script:

// executor-service.js
// This script monitors queued requests and executes them automatically

const ethers = require('ethers');

async function monitorAndExecute() {
  const provider = new ethers.JsonRpcProvider(RPC_URL);
  const operatorWallet = new ethers.Wallet(OPERATOR_PRIVATE_KEY, provider);
  const treasury = new ethers.Contract(TREASURY_ADDRESS, TREASURY_ABI, operatorWallet);
  
  console.log('🤖 Executor service started');
  console.log('Operator:', operatorWallet.address);
  
  // Listen for new queued withdrawals
  treasury.on('WithdrawalQueued', async (requestId, recipient, tokenAddress, amount, queuedAt) => {
    console.log('\n📥 New withdrawal queued');
    console.log('Request ID:', requestId);
    console.log('Amount:', ethers.formatEther(amount), 'ETH');
    
    const timelockDuration = await treasury.timelockDuration();
    const executeAt = Number(queuedAt) + Number(timelockDuration);
    const delay = (executeAt * 1000) - Date.now();
    
    console.log('Will execute at:', new Date(executeAt * 1000));
    console.log('Delay:', delay / 1000 / 60, 'minutes');
    
    // Schedule execution
    setTimeout(async () => {
      try {
        console.log('\n⚡ Executing queued withdrawal:', requestId);
        const tx = await treasury.executeQueuedWithdrawal(requestId);
        const receipt = await tx.wait();
        console.log('✅ Executed! Tx:', receipt.hash);
      } catch (error) {
        console.error('❌ Execution failed:', error.message);
      }
    }, delay + 10000); // Add 10s buffer
  });
  
  // Also check for existing queued requests on startup
  const filter = treasury.filters.WithdrawalQueued();
  const events = await treasury.queryFilter(filter, -10000); // Last 10000 blocks
  
  for (const event of events) {
    const { requestId, queuedAt } = event.args;
    const request = await treasury.queuedRequests(requestId);
    
    if (!request.executed && !request.cancelled) {
      const timelockDuration = await treasury.timelockDuration();
      const executeAt = Number(queuedAt) + Number(timelockDuration);
      const now = Math.floor(Date.now() / 1000);
      
      if (now >= executeAt) {
        console.log('\n⚡ Executing pending request:', requestId);
        try {
          const tx = await treasury.executeQueuedWithdrawal(requestId);
          await tx.wait();
          console.log('✅ Executed!');
        } catch (error) {
          console.error('❌ Failed:', error.message);
        }
      }
    }
  }
}

// Run service
monitorAndExecute().catch(console.error);

cancelQueuedRequest()

function cancelQueuedRequest(bytes32 requestId) external

Purpose: Cancel a queued withdrawal before execution
Access: Admin or the request recipient
Use case: Detected suspicious activity, changed plans, etc.

Node.js Example:

// Check request first
const requestId = "0x1234...";
const request = await treasury.queuedRequests(requestId);

console.log('Request status:');
console.log('├─ Recipient:', request.recipient);
console.log('├─ Amount:', ethers.formatEther(request.amount));
console.log('├─ Executed:', request.executed);
console.log('└─ Cancelled:', request.cancelled);

if (request.executed) {
  console.log('❌ Cannot cancel - already executed');
  return;
}

if (request.cancelled) {
  console.log('❌ Already cancelled');
  return;
}

// Cancel as admin
try {
  const tx = await treasury.cancelQueuedRequest(requestId);
  console.log('Transaction sent:', tx.hash);
  
  const receipt = await tx.wait();
  console.log('✅ Request cancelled');
  
  // Check event
  const cancelEvent = receipt.logs
    .map(log => {
      try { return treasury.interface.parseLog(log); } catch { return null; }
    })
    .find(e => e && e.name === 'RequestCancelled');
    
  if (cancelEvent) {
    console.log('Cancelled request:', cancelEvent.args.requestId);
    console.log('Cancelled by:', cancelEvent.args.cancelledBy);
  }
  
} catch (error) {
  console.error('❌ Cancellation failed:', error.message);
}

Admin & Security Functions

pause() / unpause()

function pause() external onlyRole(DEFAULT_ADMIN_ROLE)
function unpause() external onlyRole(DEFAULT_ADMIN_ROLE)

Purpose: Emergency stop mechanism
Access: Admin only
Effect: When paused, all withdrawals are blocked

Node.js Example:

// Check if paused
const isPaused = await treasury.paused();
console.log('Treasury paused:', isPaused);

// Pause (emergency situation)
if (!isPaused) {
  const pauseTx = await treasury.pause();
  await pauseTx.wait();
  console.log('🛑 Treasury paused!');
  
  // Listen for event
  treasury.on('Paused', (account) => {
    console.log('Paused by:', account);
  });
}

// Try withdrawal while paused (will fail)
try {
  await treasury.submitWithdrawal(/*...*/);
} catch (error) {
  console.log('❌ Withdrawal blocked - contract is paused');
}

// Unpause (when situation resolved)
if (isPaused) {
  const unpauseTx = await treasury.unpause();
  await unpauseTx.wait();
  console.log('✅ Treasury unpaused');
  
  treasury.on('Unpaused', (account) => {
    console.log('Unpaused by:', account);
  });
}

// Automated monitoring
async function monitorPauseStatus() {
  treasury.on('Paused', async (account) => {
    console.log('⚠️  ALERT: Treasury paused by', account);
    // Send notification (email, Slack, etc.)
    await sendAlert({
      type: 'PAUSE',
      message: `Treasury paused by ${account}`,
      timestamp: new Date()
    });
  });
  
  treasury.on('Unpaused', async (account) => {
    console.log('✅ Treasury unpaused by', account);
    await sendAlert({
      type: 'UNPAUSE',
      message: `Treasury unpaused by ${account}`,
      timestamp: new Date()
    });
  });
}

emergencyWithdraw()

function emergencyWithdraw(address tokenAddress, uint256 amount) 
    external 
    onlyRole(DEFAULT_ADMIN_ROLE)

Purpose: Extract funds in critical emergency (e.g., contract bug discovered)
Access: Admin only
Warning: Bypasses all security checks - use with extreme caution

Node.js Example:

// EMERGENCY ONLY - Use when contract is compromised
// This should be a last resort

const ADMIN_WALLET = new ethers.Wallet(ADMIN_PRIVATE_KEY, provider);
const treasury = new ethers.Contract(TREASURY_ADDRESS, TREASURY_ABI, ADMIN_WALLET);

// Step 1: Pause first (best practice)
await treasury.pause();
console.log('🛑 Treasury paused');

// Step 2: Emergency withdraw ETH
const ethBalance = await provider.getBalance(TREASURY_ADDRESS);
console.log('Emergency: Withdrawing', ethers.formatEther(ethBalance), 'ETH');

try {
  const tx = await treasury.emergencyWithdraw(
    ethers.ZeroAddress, // ETH
    ethBalance
  );
  
  await tx.wait();
  console.log('✅ Emergency ETH withdrawal complete');
  
} catch (error) {
  console.error('❌ Emergency withdrawal failed:', error.message);
}

// Step 3: Emergency withdraw ERC20 tokens
const tokens = [
  "0x55d398326f99059fF775485246999027B3197955", // USDT
  "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d"  // USDC
];

for (const tokenAddress of tokens) {
  const tokenContract = new ethers.Contract(
    tokenAddress,
    ['function balanceOf(address) view returns (uint256)', 'function symbol() view returns (string)'],
    provider
  );
  
  const balance = await tokenContract.balanceOf(TREASURY_ADDRESS);
  const symbol = await tokenContract.symbol();
  
  if (balance > 0) {
    console.log(`Emergency: Withdrawing ${symbol}`);
    const tx = await treasury.emergencyWithdraw(tokenAddress, balance);
    await tx.wait();
    console.log(`✅ ${symbol} withdrawn`);
  }
}

console.log('🚨 Emergency evacuation complete');

grantRole() / revokeRole()

function grantRole(bytes32 role, address account) external
function revokeRole(bytes32 role, address account) external

Purpose: Manage access control roles
Access: Admin only (or role admin for that specific role)

Node.js Example:

// Get role identifiers
const ADMIN_ROLE = await treasury.DEFAULT_ADMIN_ROLE();
const OPERATOR_ROLE = await treasury.OPERATOR_ROLE();

console.log('Roles:');
console.log('├─ Admin:', ADMIN_ROLE);
console.log('└─ Operator:', OPERATOR_ROLE);

// Check who has roles
async function listRoleMembers(role, roleName) {
  console.log(`\n${roleName} members:`);
  
  // Get role member count (if available)
  try {
    const memberCount = await treasury.getRoleMemberCount(role);
    for (let i = 0; i < memberCount; i++) {
      const member = await treasury.getRoleMember(role, i);
      console.log(`├─ ${member}`);
    }
  } catch {
    // If getRoleMemberCount not available, track via events
    const filter = treasury.filters.RoleGranted(role);
    const events = await treasury.queryFilter(filter);
    const members = new Set();
    
    for (const event of events) {
      members.add(event.args.account);
    }
    
    members.forEach(member => console.log(`├─ ${member}`));
  }
}

await listRoleMembers(ADMIN_ROLE, 'Admin');
await listRoleMembers(OPERATOR_ROLE, 'Operator');

// Grant operator role
const newOperator = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb";
const hasRole = await treasury.hasRole(OPERATOR_ROLE, newOperator);

if (!hasRole) {
  const tx = await treasury.grantRole(OPERATOR_ROLE, newOperator);
  await tx.wait();
  console.log('✅ Operator role granted to', newOperator);
  
  // Listen for event
  treasury.on('RoleGranted', (role, account, sender) => {
    if (role === OPERATOR_ROLE) {
      console.log(`Operator role granted to ${account} by ${sender}`);
    }
  });
}

// Revoke operator role
const oldOperator = "0x1234...";
if (await treasury.hasRole(OPERATOR_ROLE, oldOperator)) {
  const tx = await treasury.revokeRole(OPERATOR_ROLE, oldOperator);
  await tx.wait();
  console.log('❌ Operator role revoked from', oldOperator);
}

// Check if address has specific role
async function checkRoles(address) {
  const roles = {
    'Admin': await treasury.hasRole(ADMIN_ROLE, address),
    'Operator': await treasury.hasRole(OPERATOR_ROLE, address)
  };
  
  console.log(`\nRoles for ${address}:`);
  for (const [role, has] of Object.entries(roles)) {
    console.log(`├─ ${role}: ${has ? '✅' : '❌'}`);
  }
  
  return roles;
}

await checkRoles("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb");

View Functions (Read-Only)

getBalance()

function getBalance(address tokenAddress) external view returns (uint256)

Purpose: Check treasury balance for ETH or ERC20 tokens
Access: Public (anyone can read)

Node.js Example:

// Check ETH balance
const ethBalance = await treasury.getBalance(ethers.ZeroAddress);
console.log('Treasury ETH:', ethers.formatEther(ethBalance), 'ETH');

// Check ERC20 balance
const USDT = "0x55d398326f99059fF775485246999027B3197955";
const usdtBalance = await treasury.getBalance(USDT);
console.log('Treasury USDT:', ethers.formatUnits(usdtBalance, 18));

// Get all token balances
const tokens = {
  'ETH': ethers.ZeroAddress,
  'USDT': "0x55d398326f99059fF775485246999027B3197955",
  'USDC': "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
  'BUSD': "0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56"
};

console.log('\n💰 Treasury Balances:');
for (const [symbol, address] of Object.entries(tokens)) {
  try {
    const balance = await treasury.getBalance(address);
    const formatted = symbol === 'ETH' 
      ? ethers.formatEther(balance)
      : ethers.formatUnits(balance, 18);
    console.log(`├─ ${symbol}: ${formatted}`);
  } catch (error) {
    console.log(`├─ ${symbol}: Error`);
  }
}

// Monitor balance changes
async function monitorBalances() {
  let previousBalances = {};
  
  setInterval(async () => {
    for (const [symbol, address] of Object.entries(tokens)) {
      const balance = await treasury.getBalance(address);
      const balanceStr = balance.toString();
      
      if (previousBalances[symbol] && previousBalances[symbol] !== balanceStr) {
        const diff = balance - BigInt(previousBalances[symbol]);
        const formatted = symbol === 'ETH'
          ? ethers.formatEther(diff)
          : ethers.formatUnits(diff, 18);
        
        console.log(`\n💸 ${symbol} balance changed: ${formatted > 0 ? '+' : ''}${formatted}`);
      }
      
      previousBalances[symbol] = balanceStr;
    }
  }, 30000); // Check every 30 seconds
}

getTreasurySettings()

function getTreasurySettings() external view returns (
    uint256 _instantWithdrawalThreshold,
    uint256 _perUserDailyLimit,
    uint256 _globalDailyLimit,
    uint256 _timelockDuration,
    uint256 _requiredSigners
)

Purpose: Get all configuration settings at once
Access: Public

Node.js Example:

// Get all settings
const settings = await treasury.getTreasurySettings();

console.log('📋 Treasury Settings:');
console.log('├─ Instant Threshold:', ethers.formatEther(settings._instantWithdrawalThreshold), 'ETH');
console.log('├─ Per-User Daily Limit:', ethers.formatEther(settings._perUserDailyLimit), 'ETH');
console.log('├─ Global Daily Limit:', ethers.formatEther(settings._globalDailyLimit), 'ETH');
console.log('├─ Timelock Duration:', settings._timelockDuration / 3600, 'hours');
console.log('└─ Required Signers:', settings._requiredSigners.toString());

// Use destructuring
const {
  _instantWithdrawalThreshold,
  _perUserDailyLimit,
  _globalDailyLimit,
  _timelockDuration,
  _requiredSigners
} = await treasury.getTreasurySettings();

// Format for display
function formatTreasurySettings(settings) {
  return {
    instantThreshold: `${ethers.formatEther(settings._instantWithdrawalThreshold)} ETH`,
    userDailyLimit: `${ethers.formatEther(settings._perUserDailyLimit)} ETH`,
    globalDailyLimit: `${ethers.formatEther(settings._globalDailyLimit)} ETH`,
    timelockHours: Number(settings._timelockDuration) / 3600,
    requiredSigners: Number(settings._requiredSigners)
  };
}

const formatted = formatTreasurySettings(settings);
console.log(JSON.stringify(formatted, null, 2));

getUserDailyUsage()

function getUserDailyUsage(address user) external view returns (uint256, uint256)

Purpose: Get user’s daily withdrawal amount and reset time
Returns: (amount withdrawn today, last reset timestamp)

Node.js Example:

// Check user's daily usage
const userAddress = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb";
const [amount, lastReset] = await treasury.getUserDailyUsage(userAddress);

const limit = await treasury.perUserDailyLimit();
const remaining = limit - amount;
const percentUsed = (Number(amount) * 100) / Number(limit);

console.log(`\n👤 Daily Usage for ${userAddress}:`);
console.log('├─ Used:', ethers.formatEther(amount), 'ETH');
console.log('├─ Limit:', ethers.formatEther(limit), 'ETH');
console.log('├─ Remaining:', ethers.formatEther(remaining), 'ETH');
console.log('├─ Percent:', percentUsed.toFixed(2), '%');
console.log('├─ Last reset:', new Date(Number(lastReset) * 1000));

// Calculate when it resets
const now = Math.floor(Date.now() / 1000);
const nextReset = Number(lastReset) + 86400;
const hoursUntilReset = (nextReset - now) / 3600;

console.log('└─ Resets in:', hoursUntilReset.toFixed(1), 'hours');

// Check if user can make withdrawal
async function canUserWithdraw(userAddress, amount) {
  const [used, lastReset] = await treasury.getUserDailyUsage(userAddress);
  const limit = await treasury.perUserDailyLimit();
  const available = limit - used;
  
  if (amount <= available) {
    console.log(`✅ User can withdraw ${ethers.formatEther(amount)} ETH`);
    return true;
  } else {
    console.log(`❌ Insufficient daily limit`);
    console.log(`Requested: ${ethers.formatEther(amount)} ETH`);
    console.log(`Available: ${ethers.formatEther(available)} ETH`);
    
    // Check when they can withdraw after reset
    const now = Math.floor(Date.now() / 1000);
    const nextReset = Number(lastReset) + 86400;
    if (now >= nextReset) {
      console.log('✅ Limit will reset immediately on next transaction');
    } else {
      console.log(`⏳ Wait ${((nextReset - now) / 3600).toFixed(1)} hours for reset`);
    }
    
    return false;
  }
}

await canUserWithdraw(userAddress, ethers.parseEther("1000"));

getGlobalDailyUsage()

function getGlobalDailyUsage() external view returns (uint256, uint256)

Purpose: Get global daily withdrawal amount and reset time
Returns: (total amount withdrawn today, last reset timestamp)

Node.js Example:

// Check global daily usage
const [globalAmount, globalLastReset] = await treasury.getGlobalDailyUsage();
const globalLimit = await treasury.globalDailyLimit();
const globalRemaining = globalLimit - globalAmount;
const globalPercent = (Number(globalAmount) * 100) / Number(globalLimit);

console.log('\n🌍 Global Daily Usage:');
console.log('├─ Used:', ethers.formatEther(globalAmount), 'ETH');
console.log('├─ Limit:', ethers.formatEther(globalLimit), 'ETH');
console.log('├─ Remaining:', ethers.formatEther(globalRemaining), 'ETH');
console.log('├─ Percent:', globalPercent.toFixed(2), '%');
console.log('└─ Last reset:', new Date(Number(globalLastReset) * 1000));

// Warning if approaching limit
if (globalPercent > 80) {
  console.warn('⚠️  WARNING: Global daily limit almost reached!');
}

if (globalPercent > 95) {
  console.error('🚨 CRITICAL: Global daily limit nearly exhausted!');
}

// Dashboard stats
async function getDashboardStats() {
  const [globalUsed] = await treasury.getGlobalDailyUsage();
  const globalLimit = await treasury.globalDailyLimit();
  const ethBalance = await treasury.getBalance(ethers.ZeroAddress);
  const isPaused = await treasury.paused();
  
  return {
    treasury: {
      ethBalance: ethers.formatEther(ethBalance),
      paused: isPaused
    },
    dailyLimits: {
      used: ethers.formatEther(globalUsed),
      limit: ethers.formatEther(globalLimit),
      remaining: ethers.formatEther(globalLimit - globalUsed),
      percentUsed: (Number(globalUsed) * 100) / Number(globalLimit)
    },
    status: isPaused ? '🛑 PAUSED' : '✅ ACTIVE'
  };
}

const stats = await getDashboardStats();
console.log('\n📊 Dashboard:');
console.log(JSON.stringify(stats, null, 2));

🔗 Integration Patterns {#integration-patterns}

Complete Withdrawal Flow (Production-Ready)

// withdrawal-service.js
const ethers = require('ethers');

class WithdrawalService {
  constructor(treasuryAddress, provider, adminWallet) {
    this.treasury = new ethers.Contract(
      treasuryAddress,
      TREASURY_ABI,
      adminWallet
    );
    this.provider = provider;
    this.adminWallet = adminWallet;
  }
  
  // Generate unique request ID
  generateRequestId(recipient, tokenAddress, amount, expiry) {
    return ethers.keccak256(
      ethers.AbiCoder.defaultAbiCoder().encode(
        ['address', 'address', 'uint256', 'uint256', 'uint256'],
        [recipient, tokenAddress, amount, expiry, Date.now()]
      )
    );
  }
  
  // Create EIP-712 typed data
  async getTypedData(requestId, recipient, tokenAddress, amount, expiry) {
    const chainId = (await this.provider.getNetwork()).chainId;
    
    const domain = {
      name: 'QubiSafeGuardian',
      version: '2.0',
      chainId: Number(chainId),
      verifyingContract: await this.treasury.getAddress()
    };
    
    const types = {
      WithdrawalRequest: [
        { name: 'requestId', type: 'bytes32' },
        { name: 'recipient', type: 'address' },
        { name: 'tokenAddress', type: 'address' },
        { name: 'amount', type: 'uint256' },
        { name: 'expiry', type: 'uint256' }
      ]
    };
    
    const value = {
      requestId,
      recipient,
      tokenAddress,
      amount,
      expiry
    };
    
    return { domain, types, value };
  }
  
  // Collect signatures from multiple signers
  async collectSignatures(signerWallets, typedData) {
    const signatures = [];
    
    for (const wallet of signerWallets) {
      const signature = await wallet.signTypedData(
        typedData.domain,
        typedData.types,
        typedData.value
      );
      signatures.push(signature);
    }
    
    return signatures;
  }
  
  // Pre-flight checks
  async validateWithdrawal(recipient, tokenAddress, amount) {
    const errors = [];
    
    // Check if paused
    const isPaused = await this.treasury.paused();
    if (isPaused) {
      errors.push('Treasury is paused');
    }
    
    // Check token allowlist
    if (tokenAddress !== ethers.ZeroAddress) {
      const isAllowed = await this.treasury.tokenAllowlist(tokenAddress);
      if (!isAllowed) {
        errors.push('Token not in allowlist');
      }
    }
    
    // Check treasury balance
    const balance = await this.treasury.getBalance(tokenAddress);
    if (balance < amount) {
      errors.push(`Insufficient treasury balance. Has: ${ethers.formatEther(balance)}, Need: ${ethers.formatEther(amount)}`);
    }
    
    // Check user daily limit
    const [userUsed] = await this.treasury.getUserDailyUsage(recipient);
    const userLimit = await this.treasury.perUserDailyLimit();
    if (userUsed + amount > userLimit) {
      errors.push(`User daily limit exceeded. Used: ${ethers.formatEther(userUsed)}, Limit: ${ethers.formatEther(userLimit)}`);
    }
    
    // Check global daily limit
    const [globalUsed] = await this.treasury.getGlobalDailyUsage();
    const globalLimit = await this.treasury.globalDailyLimit();
    if (globalUsed + amount > globalLimit) {
      errors.push(`Global daily limit exceeded. Used: ${ethers.formatEther(globalUsed)}, Limit: ${ethers.formatEther(globalLimit)}`);
    }
    
    return {
      valid: errors.length === 0,
      errors
    };
  }
  
  // Main withdrawal function
  async submitWithdrawal(recipient, tokenAddress, amount, signerWallets, expiryMinutes = 60) {
    console.log('\n🔄 Starting withdrawal process...');
    
    // Step 1: Validate
    console.log('1️⃣  Validating withdrawal...');
    const validation = await this.validateWithdrawal(recipient, tokenAddress, amount);
    
    if (!validation.valid) {
      console.error('❌ Validation failed:');
      validation.errors.forEach(err => console.error('  -', err));
      throw new Error('Validation failed: ' + validation.errors.join(', '));
    }
    console.log('✅ Validation passed');
    
    // Step 2: Generate request ID
    const expiry = Math.floor(Date.now() / 1000) + (expiryMinutes * 60);
    const requestId = this.generateRequestId(recipient, tokenAddress, amount, expiry);
    console.log('2️⃣  Request ID:', requestId);
    
    // Step 3: Check if request ID already used
    const isUsed = await this.treasury.usedRequestIds(requestId);
    if (isUsed) {
      throw new Error('Request ID already used');
    }
    
    // Step 4: Create typed data
    console.log('3️⃣  Creating typed data...');
    const typedData = await this.getTypedData(requestId, recipient, tokenAddress, amount, expiry);
    
    // Step 5: Collect signatures
    console.log('4️⃣  Collecting signatures...');
    const signatures = await this.collectSignatures(signerWallets, typedData);
    console.log(`✅ Collected ${signatures.length} signatures`);
    
    // Step 6: Submit transaction
    console.log('5️⃣  Submitting withdrawal...');
    const tx = await this.treasury.submitWithdrawal(
      requestId,
      recipient,
      tokenAddress,
      amount,
      expiry,
      signatures
    );
    
    console.log('📤 Transaction sent:', tx.hash);
    
    // Step 7: Wait for confirmation
    console.log('6️⃣  Waiting for confirmation...');
    const receipt = await tx.wait();
    console.log('✅ Transaction confirmed!');
    console.log('Gas used:', receipt.gasUsed.toString());
    
    // Step 8: Parse events
    const events = receipt.logs
      .map(log => {
        try { return this.treasury.interface.parseLog(log); } catch { return null; }
      })
      .filter(e => e !== null);
    
    const result = { requestId, txHash: receipt.hash, events: [] };
    
    for (const event of events) {
      if (event.name === 'WithdrawalExecuted') {
        console.log('\n✅ INSTANT WITHDRAWAL EXECUTED');
        console.log('├─ Recipient:', event.args.recipient);
        console.log('├─ Amount:', ethers.formatEther(event.args.amount));
        console.log('└─ Token:', event.args.tokenAddress);
        result.status = 'executed';
        result.events.push({ type: 'executed', data: event.args });
      } else if (event.name === 'WithdrawalQueued') {
        console.log('\n⏳ WITHDRAWAL QUEUED FOR TIMELOCK');
        console.log('├─ Recipient:', event.args.recipient);
        console.log('├─ Amount:', ethers.formatEther(event.args.amount));
        console.log('├─ Queued at:', new Date(Number(event.args.queuedAt) * 1000));
        
        const timelockDuration = await this.treasury.timelockDuration();
        const executeAt = Number(event.args.queuedAt) + Number(timelockDuration);
        console.log('└─ Can execute at:', new Date(executeAt * 1000));
        
        result.status = 'queued';
        result.executeAt = executeAt;
        result.events.push({ type: 'queued', data: event.args });
      }
    }
    
    return result;
  }
}

// Usage example
async function main() {
  const provider = new ethers.JsonRpcProvider(RPC_URL);
  const adminWallet = new ethers.Wallet(ADMIN_PRIVATE_KEY, provider);
  const signer1 = new ethers.Wallet(SIGNER1_PRIVATE_KEY, provider);
  const signer2 = new ethers.Wallet(SIGNER2_PRIVATE_KEY, provider);
  
  const service = new WithdrawalService(TREASURY_ADDRESS, provider, adminWallet);
  
  // Submit withdrawal
  const result = await service.submitWithdrawal(
    "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", // recipient
    ethers.ZeroAddress,                           // ETH
    ethers.parseEther("500"),                     // 500 ETH
    [signer1, signer2],                           // signers
    60                                            // 60 minute expiry
  );
  
  console.log('\n📋 Final Result:');
  console.log(JSON.stringify(result, null, 2));
}

main().catch(console.error);

🎛️ Making Multi-Signature Optional {#optional-multisig}

Understanding the Current Design

The current contract always requires multi-signature. But you can make it easier:

Option 1: Set requiredSigners to 1 (Easiest)

This effectively disables multi-sig - only 1 signature needed:

// Configure for single signature (no multi-sig headache)
const tx = await treasury.updateTreasurySettings(
  ethers.parseEther("1000"),  // instant threshold
  ethers.parseEther("5000"),  // user daily limit
  ethers.parseEther("50000"), // global daily limit
  86400,                      // timelock (24 hours)
  1                           // ⬅️ ONLY 1 SIGNER REQUIRED!
);
await tx.wait();
console.log('✅ Multi-sig disabled - only 1 signature needed');

// Now you only need 1 signature
const signer1 = new ethers.Wallet(SIGNER1_PRIVATE_KEY, provider);

const typedData = await getTypedData(requestId, recipient, tokenAddress, amount, expiry);
const signature = await signer1.signTypedData(
  typedData.domain,
  typedData.types,
  typedData.value
);

// Submit with just ONE signature
await treasury.submitWithdrawal(
  requestId,
  recipient,
  tokenAddress,
  amount,
  expiry,
  [signature]  // ⬅️ Only 1 signature in array!
);

Option 2: Admin Pre-Signs Everything (Hybrid Approach)

Admin can pre-sign requests for operators to submit later:

// admin-signer-service.js
// This service runs on admin's secure server
// It auto-signs withdrawal requests and stores signatures in database

const express = require('express');
const app = express();

// Admin wallet (kept secure)
const adminWallet = new ethers.Wallet(ADMIN_PRIVATE_KEY, provider);

// API endpoint to get admin signature
app.post('/api/sign-withdrawal', async (req, res) => {
  const { requestId, recipient, tokenAddress, amount, expiry } = req.body;
  
  // Optional: Add approval logic here
  // e.g., check amount limits, whitelist recipients, etc.
  if (amount > ethers.parseEther("10000")) {
    return res.status(403).json({ error: 'Amount too high - needs manual approval' });
  }
  
  // Create typed data
  const domain = {
    name: 'QubiSafeGuardian',
    version: '2.0',
    chainId: 97,
    verifyingContract: TREASURY_ADDRESS
  };
  
  const types = {
    WithdrawalRequest: [
      { name: 'requestId', type: 'bytes32' },
      { name: 'recipient', type: 'address' },
      { name: 'tokenAddress', type: 'address' },
      { name: 'amount', type: 'uint256' },
      { name: 'expiry', type: 'uint256' }
    ]
  };
  
  const value = { requestId, recipient, tokenAddress, amount, expiry };
  
  // Sign it
  const signature = await adminWallet.signTypedData(domain, types, value);
  
  // Log for audit
  console.log('Admin signed withdrawal:', { requestId, recipient, amount });
  
  res.json({ signature, signer: adminWallet.address });
});

app.listen(3000, () => console.log('Admin signer service running on port 3000'));
// operator-client.js
// Operators call this API to get admin signature automatically

async function submitWithdrawalWithAutoSign(recipient, tokenAddress, amount) {
  const expiry = Math.floor(Date.now() / 1000) + 3600;
  const requestId = generateRequestId(recipient, tokenAddress, amount, expiry);
  
  // Get admin signature automatically from API
  const response = await fetch('http://admin-server:3000/api/sign-withdrawal', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ requestId, recipient, tokenAddress, amount, expiry })
  });
  
  const { signature: adminSignature } = await response.json();
  
  // Operator adds their own signature
  const operatorWallet = new ethers.Wallet(OPERATOR_PRIVATE_KEY, provider);
  const typedData = await getTypedData(requestId, recipient, tokenAddress, amount, expiry);
  const operatorSignature = await operatorWallet.signTypedData(
    typedData.domain,
    typedData.types,
    typedData.value
  );
  
  // Submit with both signatures
  const tx = await treasury.submitWithdrawal(
    requestId,
    recipient,
    tokenAddress,
    amount,
    expiry,
    [adminSignature, operatorSignature]
  );
  
  await tx.wait();
  console.log('✅ Withdrawal submitted with auto-signed admin signature');
}

Option 3: Trusted Operator Can Submit Without External Signatures

If you trust your backend, use a single wallet that’s both signer and submitter:

// single-wallet-withdrawal.js
// One wallet does everything

const trustedWallet = new ethers.Wallet(TRUSTED_PRIVATE_KEY, provider);
const treasury = new ethers.Contract(TREASURY_ADDRESS, TREASURY_ABI, trustedWallet);

// Make sure this wallet is an authorized signer
await treasury.updateSigner(trustedWallet.address, true);

// Set required signers to 1
await treasury.updateTreasurySettings(
  ethers.parseEther("1000"),
  ethers.parseEther("5000"),
  ethers.parseEther("50000"),
  86400,
  1  // Only 1 signature needed
);

// Now you can submit withdrawals easily
async function easyWithdrawal(recipient, amount) {
  const requestId = generateRequestId(recipient, ethers.ZeroAddress, amount, expiry);
  const expiry = Math.floor(Date.now() / 1000) + 3600;
  
  const typedData = await getTypedData(requestId, recipient, ethers.ZeroAddress, amount, expiry);
  
  // Sign with the same wallet
  const signature = await trustedWallet.signTypedData(
    typedData.domain,
    typedData.types,
    typedData.value
  );
  
  // Submit
  await treasury.submitWithdrawal(
    requestId,
    recipient,
    ethers.ZeroAddress,
    amount,
    expiry,
    [signature]  // Just one signature!
  );
  
  console.log('✅ Withdrawal submitted easily');
}

await easyWithdrawal("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", ethers.parseEther("100"));

🤖 Automatic vs Manual Withdrawals {#auto-manual-withdrawals}

Understanding the Current Behavior

The contract has automatic decision-making based on amount:

But you want more control! Here’s how to implement flags for auto/manual:

Implementation: Add Withdrawal Flags in Your Backend

// withdrawal-manager.js
// This adds auto/manual flags in your Node.js backend

class WithdrawalManager {
  constructor(treasury, provider) {
    this.treasury = treasury;
    this.provider = provider;
    this.db = {}; // Use real database in production
  }
  
  /**
   * Submit withdrawal with auto/manual flag
   * @param {Object} params
   * @param {string} params.recipient - Recipient address
   * @param {string} params.tokenAddress - Token address (use ZeroAddress for ETH)
   * @param {BigInt} params.amount - Amount to withdraw
   * @param {Array} params.signerWallets - Signer wallets
   * @param {boolean} params.autoExecute - TRUE = automatic, FALSE = manual approval needed
   * @param {boolean} params.bypassTimelock - TRUE = force instant even if large amount
   */
  async submitWithdrawal({ 
    recipient, 
    tokenAddress, 
    amount, 
    signerWallets, 
    autoExecute = true,
    bypassTimelock = false 
  }) {
    const expiry = Math.floor(Date.now() / 1000) + 3600;
    const requestId = this.generateRequestId(recipient, tokenAddress, amount, expiry);
    
    console.log('\n🔄 Withdrawal Request:');
    console.log('├─ Recipient:', recipient);
    console.log('├─ Amount:', ethers.formatEther(amount), 'ETH');
    console.log('├─ Auto-execute:', autoExecute ? '✅ YES' : '❌ NO (Manual)');
    console.log('└─ Bypass timelock:', bypassTimelock ? '✅ YES' : '❌ NO');
    
    // MANUAL FLAG: Store in database for approval workflow
    if (!autoExecute) {
      await this.storeForManualApproval({
        requestId,
        recipient,
        tokenAddress,
        amount,
        status: 'PENDING_APPROVAL',
        createdAt: Date.now(),
        approvedBy: null
      });
      
      console.log('⏸️  Withdrawal stored for MANUAL approval');
      console.log('An admin must approve this in the dashboard');
      
      return {
        status: 'pending_manual_approval',
        requestId,
        message: 'Waiting for admin approval in dashboard'
      };
    }
    
    // AUTO FLAG: Proceed automatically
    console.log('🤖 Proceeding with AUTOMATIC withdrawal');
    
    // Check if amount would normally be queued
    const threshold = await this.treasury.instantWithdrawalThreshold();
    
    if (amount > threshold && !bypassTimelock) {
      console.log('⚠️  Amount exceeds instant threshold');
      console.log('This will be queued for timelock (24 hours)');
      
      // You can choose to:
      // A) Still proceed (let it queue)
      // B) Ask for manual approval
      // C) Cancel
      
      const userChoice = bypassTimelock ? 'proceed' : 'queue';
      
      if (userChoice === 'queue') {
        console.log('📋 Proceeding with QUEUED withdrawal');
        // Fallthrough to submission
      }
    }
    
    // Create typed data and signatures
    const typedData = await this.getTypedData(requestId, recipient, tokenAddress, amount, expiry);
    const signatures = await this.collectSignatures(signerWallets, typedData);
    
    // Submit to blockchain
    const tx = await this.treasury.submitWithdrawal(
      requestId,
      recipient,
      tokenAddress,
      amount,
      expiry,
      signatures
    );
    
    const receipt = await tx.wait();
    
    // Check if instant or queued
    const events = receipt.logs
      .map(log => {
        try { return this.treasury.interface.parseLog(log); } catch { return null; }
      })
      .filter(e => e !== null);
    
    for (const event of events) {
      if (event.name === 'WithdrawalExecuted') {
        console.log('✅ AUTOMATIC INSTANT EXECUTION');
        return {
          status: 'executed',
          requestId,
          txHash: receipt.hash,
          executionType: 'instant'
        };
      } else if (event.name === 'WithdrawalQueued') {
        const executeAt = Number(event.args.queuedAt) + Number(await this.treasury.timelockDuration());
        console.log('⏳ AUTOMATIC QUEUED (needs operator execution after timelock)');
        
        // Store in database for auto-executor service
        await this.storeQueuedRequest({
          requestId,
          executeAt,
          autoExecute: true // Flag for auto-executor
        });
        
        return {
          status: 'queued',
          requestId,
          txHash: receipt.hash,
          executeAt: new Date(executeAt * 1000),
          executionType: 'queued_auto'
        };
      }
    }
  }
  
  // Store withdrawal for manual approval
  async storeForManualApproval(data) {
    this.db[data.requestId] = {
      ...data,
      type: 'manual_approval_pending'
    };
    
    // In production: Save to database
    // await db.withdrawals.create(data);
    
    // Send notification to admins
    await this.notifyAdmins({
      type: 'WITHDRAWAL_NEEDS_APPROVAL',
      requestId: data.requestId,
      amount: ethers.formatEther(data.amount),
      recipient: data.recipient
    });
  }
  
  // Admin approves manual withdrawal
  async approveManualWithdrawal(requestId, adminAddress) {
    const withdrawal = this.db[requestId];
    
    if (!withdrawal) {
      throw new Error('Withdrawal not found');
    }
    
    if (withdrawal.status !== 'PENDING_APPROVAL') {
      throw new Error('Withdrawal already processed');
    }
    
    console.log(`\n👍 Admin ${adminAddress} approved withdrawal ${requestId}`);
    
    // Update status
    withdrawal.status = 'APPROVED';
    withdrawal.approvedBy = adminAddress;
    withdrawal.approvedAt = Date.now();
    
    // Now submit to blockchain
    console.log('🚀 Submitting approved withdrawal to blockchain...');
    
    // Get signatures and submit
    // (Implementation same as above)
    
    return { status: 'approved_and_submitted' };
  }
  
  // Admin rejects manual withdrawal
  async rejectManualWithdrawal(requestId, adminAddress, reason) {
    const withdrawal = this.db[requestId];
    
    if (!withdrawal) {
      throw new Error('Withdrawal not found');
    }
    
    withdrawal.status = 'REJECTED';
    withdrawal.rejectedBy = adminAddress;
    withdrawal.rejectedAt = Date.now();
    withdrawal.rejectionReason = reason;
    
    console.log(`\n❌ Admin ${adminAddress} rejected withdrawal ${requestId}`);
    console.log('Reason:', reason);
    
    return { status: 'rejected' };
  }
  
  // Helper methods
  generateRequestId(recipient, tokenAddress, amount, expiry) {
    return ethers.keccak256(
      ethers.AbiCoder.defaultAbiCoder().encode(
        ['address', 'address', 'uint256', 'uint256', 'uint256'],
        [recipient, tokenAddress, amount, expiry, Date.now()]
      )
    );
  }
  
  async getTypedData(requestId, recipient, tokenAddress, amount, expiry) {
    const chainId = (await this.provider.getNetwork()).chainId;
    
    return {
      domain: {
        name: 'QubiSafeGuardian',
        version: '2.0',
        chainId: Number(chainId),
        verifyingContract: await this.treasury.getAddress()
      },
      types: {
        WithdrawalRequest: [
          { name: 'requestId', type: 'bytes32' },
          { name: 'recipient', type: 'address' },
          { name: 'tokenAddress', type: 'address' },
          { name: 'amount', type: 'uint256' },
          { name: 'expiry', type: 'uint256' }
        ]
      },
      value: { requestId, recipient, tokenAddress, amount, expiry }
    };
  }
  
  async collectSignatures(signerWallets, typedData) {
    const signatures = [];
    for (const wallet of signerWallets) {
      const sig = await wallet.signTypedData(
        typedData.domain,
        typedData.types,
        typedData.value
      );
      signatures.push(sig);
    }
    return signatures;
  }
  
  async notifyAdmins(notification) {
    // Implement your notification system
    console.log('📧 Notification sent to admins:', notification);
    // e.g., send email, Slack message, push notification
  }
  
  async storeQueuedRequest(data) {
    this.db[data.requestId] = {
      ...this.db[data.requestId],
      ...data,
      type: 'queued_for_execution'
    };
  }
}

// Usage Examples
async function examples() {
  const provider = new ethers.JsonRpcProvider(RPC_URL);
  const treasury = new ethers.Contract(TREASURY_ADDRESS, TREASURY_ABI, provider);
  const manager = new WithdrawalManager(treasury, provider);
  
  const signer1 = new ethers.Wallet(SIGNER1_PRIVATE_KEY, provider);
  const signer2 = new ethers.Wallet(SIGNER2_PRIVATE_KEY, provider);
  
  // Example 1: AUTOMATIC small withdrawal
  console.log('\n📝 Example 1: Automatic Small Withdrawal');
  await manager.submitWithdrawal({
    recipient: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
    tokenAddress: ethers.ZeroAddress,
    amount: ethers.parseEther("100"),
    signerWallets: [signer1, signer2],
    autoExecute: true  // ✅ AUTOMATIC
  });
  
  // Example 2: MANUAL approval required
  console.log('\n📝 Example 2: Manual Approval Required');
  const result = await manager.submitWithdrawal({
    recipient: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
    tokenAddress: ethers.ZeroAddress,
    amount: ethers.parseEther("100"),
    signerWallets: [signer1, signer2],
    autoExecute: false  // ❌ MANUAL - needs approval
  });
  
  console.log('Result:', result);
  // { status: 'pending_manual_approval', requestId: '0x...', message: '...' }
  
  // Admin approves it later
  await manager.approveManualWithdrawal(result.requestId, ADMIN_ADDRESS);
  
  // Example 3: AUTOMATIC large withdrawal (will be queued)
  console.log('\n📝 Example 3: Automatic Large Withdrawal (Queued)');
  await manager.submitWithdrawal({
    recipient: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
    tokenAddress: ethers.ZeroAddress,
    amount: ethers.parseEther("5000"), // Above threshold
    signerWallets: [signer1, signer2],
    autoExecute: true,     // ✅ AUTOMATIC
    bypassTimelock: false  // ❌ Don't bypass - let it queue
  });
  
  // Example 4: FORCE instant execution (bypass timelock)
  // WARNING: This doesn't actually work with current contract!
  // You'd need to modify the contract to support this
  console.log('\n📝 Example 4: Force Instant (Not supported by contract)');
  console.log('⚠️  Current contract always queues amounts > threshold');
  console.log('To support this, you need to modify the smart contract');
}

examples().catch(console.error);

Auto-Executor Service for Queued Withdrawals

// auto-executor-service.js
// This service automatically executes queued withdrawals after timelock

class AutoExecutorService {
  constructor(treasury, operatorWallet) {
    this.treasury = treasury.connect(operatorWallet);
    this.operatorWallet = operatorWallet;
    this.queuedRequests = new Map();
  }
  
  async start() {
    console.log('🤖 Auto-Executor Service Started');
    console.log('Operator:', this.operatorWallet.address);
    
    // Listen for new queued withdrawals
    this.treasury.on('WithdrawalQueued', async (requestId, recipient, tokenAddress, amount, queuedAt) => {
      console.log('\n📥 New queued withdrawal detected');
      console.log('Request ID:', requestId);
      
      const timelockDuration = await this.treasury.timelockDuration();
      const executeAt = Number(queuedAt) + Number(timelockDuration);
      
      // Check if this should be auto-executed
      // (Check your database flag)
      const shouldAutoExecute = true; // Get from DB
      
      if (shouldAutoExecute) {
        this.scheduleExecution(requestId, executeAt);
      } else {
        console.log('⏸️  Manual execution required - skipping auto-execute');
      }
    });
    
    // Check for existing queued requests
    await this.checkExistingQueued();
    
    // Periodic check every 5 minutes
    setInterval(() => this.checkExistingQueued(), 5 * 60 * 1000);
  }
  
  scheduleExecution(requestId, executeAt) {
    const now = Math.floor(Date.now() / 1000);
    const delay = (executeAt - now + 30) * 1000; // Add 30s buffer
    
    console.log(`⏰ Scheduled execution for ${new Date(executeAt * 1000)}`);
    console.log(`   Delay: ${delay / 1000 / 60} minutes`);
    
    if (delay < 0) {
      // Already executable
      this.executeNow(requestId);
    } else {
      // Schedule for later
      const timeoutId = setTimeout(() => {
        this.executeNow(requestId);
      }, delay);
      
      this.queuedRequests.set(requestId, {
        executeAt,
        timeoutId,
        status: 'scheduled'
      });
    }
  }
  
  async executeNow(requestId) {
    try {
      console.log(`\n⚡ Executing queued withdrawal: ${requestId}`);
      
      // Double-check it's ready
      const request = await this.treasury.queuedRequests(requestId);
      
      if (request.executed) {
        console.log('✅ Already executed');
        return;
      }
      
      if (request.cancelled) {
        console.log('❌ Cancelled');
        return;
      }
      
      const timelockDuration = await this.treasury.timelockDuration();
      const canExecuteAt = Number(request.queuedAt) + Number(timelockDuration);
      const now = Math.floor(Date.now() / 1000);
      
      if (now < canExecuteAt) {
        console.log('⏳ Timelock not expired yet - rescheduling');
        this.scheduleExecution(requestId, canExecuteAt);
        return;
      }
      
      // Execute
      const tx = await this.treasury.executeQueuedWithdrawal(requestId);
      console.log('📤 Transaction sent:', tx.hash);
      
      const receipt = await tx.wait();
      console.log('✅ Executed successfully!');
      console.log('Gas used:', receipt.gasUsed.toString());
      
      // Remove from scheduled
      this.queuedRequests.delete(requestId);
      
      // Update database
      // await db.withdrawals.update({ requestId }, { status: 'EXECUTED' });
      
    } catch (error) {
      console.error('❌ Execution failed:', error.message);
      
      // Retry logic
      console.log('🔄 Will retry in 5 minutes');
      setTimeout(() => this.executeNow(requestId), 5 * 60 * 1000);
    }
  }
  
  async checkExistingQueued() {
    console.log('\n🔍 Checking for existing queued withdrawals...');
    
    const filter = this.treasury.filters.WithdrawalQueued();
    const events = await this.treasury.queryFilter(filter, -50000); // Last 50k blocks
    
    for (const event of events) {
      const { requestId, queuedAt } = event.args;
      
      // Skip if already scheduled
      if (this.queuedRequests.has(requestId)) continue;
      
      // Check if still pending
      const request = await this.treasury.queuedRequests(requestId);
      
      if (!request.executed && !request.cancelled) {
        const timelockDuration = await this.treasury.timelockDuration();
        const executeAt = Number(queuedAt) + Number(timelockDuration);
        
        console.log(`Found pending request: ${requestId}`);
        this.scheduleExecution(requestId, executeAt);
      }
    }
  }
  
  stop() {
    console.log('🛑 Stopping auto-executor service');
    
    // Clear all scheduled executions
    for (const [requestId, data] of this.queuedRequests) {
      if (data.timeoutId) {
        clearTimeout(data.timeoutId);
      }
    }
    
    this.queuedRequests.clear();
    this.treasury.removeAllListeners();
  }
}

// Run the service
async function main() {
  const provider = new ethers.JsonRpcProvider(RPC_URL);
  const operatorWallet = new ethers.Wallet(OPERATOR_PRIVATE_KEY, provider);
  const treasury = new ethers.Contract(TREASURY_ADDRESS, TREASURY_ABI, provider);
  
  const service = new AutoExecutorService(treasury, operatorWallet);
  await service.start();
  
  // Keep running
  console.log('Service running... Press Ctrl+C to stop');
  
  // Graceful shutdown
  process.on('SIGINT', () => {
    console.log('\n⚠️  Received SIGINT signal');
    service.stop();
    process.exit(0);
  });
}

main().catch(console.error);

Dashboard API for Manual Approvals

// dashboard-api.js
// Express API for admin dashboard

const express = require('express');
const app = express();
app.use(express.json());

const withdrawalManager = new WithdrawalManager(treasury, provider);

// Get all pending manual approvals
app.get('/api/withdrawals/pending', async (req, res) => {
  const pending = Object.values(withdrawalManager.db)
    .filter(w => w.status === 'PENDING_APPROVAL')
    .map(w => ({
      requestId: w.requestId,
      recipient: w.recipient,
      amount: ethers.formatEther(w.amount),
      token: w.tokenAddress === ethers.ZeroAddress ? 'ETH' : w.tokenAddress,
      createdAt: new Date(w.createdAt),
      type: 'Manual Approval Required'
    }));
  
  res.json({ pending, count: pending.length });
});

// Approve withdrawal
app.post('/api/withdrawals/:requestId/approve', async (req, res) => {
  const { requestId } = req.params;
  const { adminAddress } = req.body;
  
  try {
    const result = await withdrawalManager.approveManualWithdrawal(requestId, adminAddress);
    res.json({ success: true, ...result });
  } catch (error) {
    res.status(400).json({ success: false, error: error.message });
  }
});

// Reject withdrawal
app.post('/api/withdrawals/:requestId/reject', async (req, res) => {
  const { requestId } = req.params;
  const { adminAddress, reason } = req.body;
  
  try {
    const result = await withdrawalManager.rejectManualWithdrawal(requestId, adminAddress, reason);
    res.json({ success: true, ...result });
  } catch (error) {
    res.status(400).json({ success: false, error: error.message });
  }
});

// Get withdrawal status
app.get('/api/withdrawals/:requestId', async (req, res) => {
  const { requestId } = req.params;
  const withdrawal = withdrawalManager.db[requestId];
  
  if (!withdrawal) {
    return res.status(404).json({ error: 'Withdrawal not found' });
  }
  
  res.json(withdrawal);
});

app.listen(4000, () => {
  console.log('📊 Dashboard API running on port 4000');
});

Summary: Auto vs Manual Flags

// QUICK REFERENCE

// 1. FULLY AUTOMATIC (no manual intervention)
await manager.submitWithdrawal({
  recipient: "0x...",
  tokenAddress: ethers.ZeroAddress,
  amount: ethers.parseEther("100"),
  signerWallets: [signer1, signer2],
  autoExecute: true  // ✅ Goes straight through
});

// 2. MANUAL APPROVAL REQUIRED (stops for admin review)
await manager.submitWithdrawal({
  recipient: "0x...",
  tokenAddress: ethers.ZeroAddress,
  amount: ethers.parseEther("100"),
  signerWallets: [signer1, signer2],
  autoExecute: false  // ❌ Waits for admin approval
});

// 3. AUTOMATIC but QUEUED (for large amounts)
await manager.submitWithdrawal({
  recipient: "0x...",
  tokenAddress: ethers.ZeroAddress,
  amount: ethers.parseEther("5000"), // Above threshold
  signerWallets: [signer1, signer2],
  autoExecute: true  // ✅ Auto-executes after 24h timelock
});

// 4. SET REQUIRED SIGNERS TO 1 (effectively disable multi-sig)
await treasury.updateTreasurySettings(
  ethers.parseEther("1000"),
  ethers.parseEther("5000"),
  ethers.parseEther("50000"),
  86400,
  1  // ⬅️ Only 1 signature needed!
);

🎯 Real-World Examples {#examples}

Complete Production Example

// production-treasury-api.js
// Full production-ready implementation

const express = require('express');
const ethers = require('ethers');
const winston = require('winston');

// Logger setup
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
    new winston.transports.Console({ format: winston.format.simple() })
  ]
});

// Configuration
const config = {
  rpcUrl: process.env.RPC_URL,
  treasuryAddress: process.env.TREASURY_ADDRESS,
  adminPrivateKey: process.env.ADMIN_PRIVATE_KEY,
  signer1PrivateKey: process.env.SIGNER1_PRIVATE_KEY,
  signer2PrivateKey: process.env.SIGNER2_PRIVATE_KEY,
  operatorPrivateKey: process.env.OPERATOR_PRIVATE_KEY
};

// Initialize
const provider = new ethers.JsonRpcProvider(config.rpcUrl);
const adminWallet = new ethers.Wallet(config.adminPrivateKey, provider);
const signer1 = new ethers.Wallet(config.signer1PrivateKey, provider);
const signer2 = new ethers.Wallet(config.signer2PrivateKey, provider);
const operatorWallet = new ethers.Wallet(config.operatorPrivateKey, provider);

const treasury = new ethers.Contract(config.treasuryAddress, TREASURY_ABI, adminWallet);

const app = express();
app.use(express.json());

// Middleware: Authentication
app.use((req, res, next) => {
  const apiKey = req.headers['x-api-key'];
  if (apiKey !== process.env.API_KEY) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
});

// POST /api/withdraw - Submit withdrawal
app.post('/api/withdraw', async (req, res) => {
  try {
    const { recipient, tokenAddress, amount, autoExecute = true } = req.body;
    
    // Validation
    if (!ethers.isAddress(recipient)) {
      return res.status(400).json({ error: 'Invalid recipient address' });
    }
    
    if (tokenAddress !== 'ETH' && !ethers.isAddress(tokenAddress)) {
      return res.status(400).json({ error: 'Invalid token address' });
    }
    
    const amountWei = ethers.parseEther(amount.toString());
    const tokenAddr = tokenAddress === 'ETH' ? ethers.ZeroAddress : tokenAddress;
    
    logger.info('Withdrawal request received', { recipient, amount, tokenAddress, autoExecute });
    
    // Pre-flight checks
    const validation = await validateWithdrawal(recipient, tokenAddr, amountWei);
    if (!validation.valid) {
      return res.status(400).json({ error: 'Validation failed', details: validation.errors });
    }
    
    // Generate request
    const expiry = Math.floor(Date.now() / 1000) + 3600;
    const requestId = generateRequestId(recipient, tokenAddr, amountWei, expiry);
    
    // Get signatures
    const typedData = await getTypedData(requestId, recipient, tokenAddr, amountWei, expiry);
    const signatures = [
      await signer1.signTypedData(typedData.domain, typedData.types, typedData.value),
      await signer2.signTypedData(typedData.domain, typedData.types, typedData.value)
    ];
    
    // Submit
    const tx = await treasury.submitWithdrawal(
      requestId,
      recipient,
      tokenAddr,
      amountWei,
      expiry,
      signatures
    );
    
    const receipt = await tx.wait();
    
    // Parse result
    const events = receipt.logs
      .map(log => {
        try { return treasury.interface.parseLog(log); } catch { return null; }
      })
      .filter(e => e !== null);
    
    let status = 'unknown';
    for (const event of events) {
      if (event.name === 'WithdrawalExecuted') {
        status = 'executed';
      } else if (event.name === 'WithdrawalQueued') {
        status = 'queued';
      }
    }
    
    logger.info('Withdrawal submitted', { requestId, status, txHash: receipt.hash });
    
    res.json({
      success: true,
      requestId,
      status,
      txHash: receipt.hash,
      gasUsed: receipt.gasUsed.toString()
    });
    
  } catch (error) {
    logger.error('Withdrawal failed', { error: error.message, stack: error.stack });
    res.status(500).json({ error: error.message });
  }
});

// GET /api/balance - Get treasury balances
app.get('/api/balance', async (req, res) => {
  try {
    const ethBalance = await treasury.getBalance(ethers.ZeroAddress);
    
    const tokens = {
      'USDT': '0x55d398326f99059fF775485246999027B3197955',
      'USDC': '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d'
    };
    
    const balances = {
      ETH: ethers.formatEther(ethBalance)
    };
    
    for (const [symbol, address] of Object.entries(tokens)) {
      try {
        const balance = await treasury.getBalance(address);
        balances[symbol] = ethers.formatUnits(balance, 18);
      } catch {
        balances[symbol] = '0';
      }
    }
    
    res.json({ balances });
  } catch (error) {
    logger.error('Failed to get balances', { error: error.message });
    res.status(500).json({ error: error.message });
  }
});

// GET /api/status - Get treasury status
app.get('/api/status', async (req, res) => {
  try {
    const [
      settings,
      isPaused,
      [globalUsed, globalLastReset],
      ethBalance
    ] = await Promise.all([
      treasury.getTreasurySettings(),
      treasury.paused(),
      treasury.getGlobalDailyUsage(),
      treasury.getBalance(ethers.ZeroAddress)
    ]);
    
    res.json({
      paused: isPaused,
      balance: {
        eth: ethers.formatEther(ethBalance)
      },
      settings: {
        instantThreshold: ethers.formatEther(settings._instantWithdrawalThreshold),
        perUserDailyLimit: ethers.formatEther(settings._perUserDailyLimit),
        globalDailyLimit: ethers.formatEther(settings._globalDailyLimit),
        timelockHours: Number(settings._timelockDuration) / 3600,
        requiredSigners: Number(settings._requiredSigners)
      },
      dailyUsage: {
        used: ethers.formatEther(globalUsed),
        limit: ethers.formatEther(settings._globalDailyLimit),
        remaining: ethers.formatEther(settings._globalDailyLimit - globalUsed),
        percentUsed: (Number(globalUsed) * 100) / Number(settings._globalDailyLimit)
      }
    });
  } catch (error) {
    logger.error('Failed to get status', { error: error.message });
    res.status(500).json({ error: error.message });
  }
});

// POST /api/admin/pause - Emergency pause
app.post('/api/admin/pause', async (req, res) => {
  try {
    const tx = await treasury.pause();
    await tx.wait();
    
    logger.warn('Treasury paused', { by: adminWallet.address });
    
    res.json({ success: true, message: 'Treasury paused' });
  } catch (error) {
    logger.error('Failed to pause', { error: error.message });
    res.status(500).json({ error: error.message });
  }
});

// POST /api/admin/unpause - Resume operations
app.post('/api/admin/unpause', async (req, res) => {
  try {
    const tx = await treasury.unpause();
    await tx.wait();
    
    logger.info('Treasury unpaused', { by: adminWallet.address });
    
    res.json({ success: true, message: 'Treasury unpaused' });
  } catch (error) {
    logger.error('Failed to unpause', { error: error.message });
    res.status(500).json({ error: error.message });
  }
});

// Helper functions
function generateRequestId(recipient, tokenAddress, amount, expiry) {
  return ethers.keccak256(
    ethers.AbiCoder.defaultAbiCoder().encode(
      ['address', 'address', 'uint256', 'uint256', 'uint256'],
      [recipient, tokenAddress, amount, expiry, Date.now()]
    )
  );
}

async function getTypedData(requestId, recipient, tokenAddress, amount, expiry) {
  const chainId = (await provider.getNetwork()).chainId;
  
  return {
    domain: {
      name: 'QubiSafeGuardian',
      version: '2.0',
      chainId: Number(chainId),
      verifyingContract: config.treasuryAddress
    },
    types: {
      WithdrawalRequest: [
        { name: 'requestId', type: 'bytes32' },
        { name: 'recipient', type: 'address' },
        { name: 'tokenAddress', type: 'address' },
        { name: 'amount', type: 'uint256' },
        { name: 'expiry', type: 'uint256' }
      ]
    },
    value: { requestId, recipient, tokenAddress, amount, expiry }
  };
}

async function validateWithdrawal(recipient, tokenAddress, amount) {
  const errors = [];
  
  const isPaused = await treasury.paused();
  if (isPaused) errors.push('Treasury is paused');
  
  if (tokenAddress !== ethers.ZeroAddress) {
    const isAllowed = await treasury.tokenAllowlist(tokenAddress);
    if (!isAllowed) errors.push('Token not allowed');
  }
  
  const balance = await treasury.getBalance(tokenAddress);
  if (balance < amount) errors.push('Insufficient treasury balance');
  
  return { valid: errors.length === 0, errors };
}

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  logger.info(`Treasury API running on port ${PORT}`);
});