Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ankit-bista/Final-Project/llms.txt

Use this file to discover all available pages before exploring further.

Every wallet address that uses Blockchain Drive has a storage quota recorded directly on the BlockchainDriveUnified smart contract. The contract is the single authoritative source for how many bytes a wallet is allowed to use, how many it has consumed, and whether an upload should be permitted. The Express backend reads and writes this state through the STORAGE_ALLOC_CONTRACT ABI, coordinating with a separate drive-level quota stored in the database so that both layers can independently block an upload when a limit is reached.

The UserQuota struct

Each wallet’s quota state is stored in a UserQuota struct in the quotas mapping on the contract:
contracts/BlockchainDriveUnified.sol
struct UserQuota {
    string tier;
    uint256 quotaLimitBytes;
    uint256 usedBytes;
    uint8 usagePercent;
    uint256 filesUploaded;
    uint256 maxFiles;
    bool isActive;
    uint256 lastUpdated;
}

mapping(address => UserQuota) public quotas;
FieldTypeDescription
tierstringLabel for the quota plan. Set to "CUSTOM" by allocateUserQuota.
quotaLimitBytesuint256Maximum bytes the wallet may store.
usedBytesuint256Bytes currently consumed across all uploaded files.
usagePercentuint8(usedBytes * 100) / quotaLimitBytes, recalculated on every mutation.
filesUploadeduint256Running count of files uploaded by this wallet.
maxFilesuint256Maximum number of files allowed. Set to type(uint256).max on first allocation if not already set.
isActiveboolMust be true for updateQuotaAfterUpload to succeed.
lastUpdateduint256Block timestamp of the last quota mutation.
A wallet with no allocated quota has isActive = false. Uploading with ENFORCE_QUOTA_ON_UPLOAD=true will fail for these wallets because updateQuotaAfterUpload reverts when the quota is not active.

Quota lifecycle

Quota moves through three transitions during normal use:
1

Admin allocates quota

An admin calls POST /api/admin/quota/allocate with a poolName, userAddress, and bytesAmount. The backend signs and submits an allocateUserQuota transaction to the contract using ADMIN_PRIVATE_KEY. The contract sets tier = "CUSTOM", quotaLimitBytes = bytesAmount, and isActive = true, then emits UserQuotaAllocated(poolName, userAddress, bytesAmount).
// POST /api/admin/quota/allocate
{
  "poolName": "standard-plan",
  "userAddress": "0xYourWalletAddress",
  "bytesAmount": 5368709120
}
bytesAmount is denominated in bytes. For example, 5368709120 is exactly 5 GiB. The contract stores the raw value — no conversion is applied.
2

Upload calls updateQuotaAfterUpload

After a file is successfully pinned to IPFS, the backend calls updateQuotaAfterUpload(walletAddress, fileSizeBytes) on the contract. The contract reverts if isActive is false or if usedBytes + fileSizeBytes would exceed quotaLimitBytes. On success it increments usedBytes and filesUploaded, recalculates usagePercent, updates lastUpdated, and emits QuotaUpdated(userAddress, usedBytes, quotaLimitBytes).
contracts/BlockchainDriveUnified.sol
function updateQuotaAfterUpload(address userAddress, uint256 fileSizeBytes) external {
    UserQuota storage q = quotas[userAddress];
    require(q.isActive, "Quota not active");
    require(q.usedBytes + fileSizeBytes <= q.quotaLimitBytes, "Quota exceeded");

    q.usedBytes += fileSizeBytes;
    q.filesUploaded += 1;
    q.usagePercent = q.quotaLimitBytes == 0
        ? 0
        : uint8((q.usedBytes * 100) / q.quotaLimitBytes);
    q.lastUpdated = block.timestamp;

    emit QuotaUpdated(userAddress, q.usedBytes, q.quotaLimitBytes);
}
3

Delete calls refundQuota

When a file is deleted, the backend calls refundQuota(walletAddress, fileSizeBytes). Unlike updateQuotaAfterUpload, this function does not revert on underflow — if the refund would make usedBytes negative, it clamps to zero. filesUploaded is decremented by one (clamped at zero) and usagePercent is recalculated. The function emits QuotaRefunded(userAddress, refundedBytes).
contracts/BlockchainDriveUnified.sol
function refundQuota(address userAddress, uint256 fileSizeBytes) external {
    UserQuota storage q = quotas[userAddress];
    if (fileSizeBytes >= q.usedBytes) {
        q.usedBytes = 0;
    } else {
        q.usedBytes -= fileSizeBytes;
    }
    if (q.filesUploaded > 0) {
        q.filesUploaded -= 1;
    }
    q.usagePercent = q.quotaLimitBytes == 0
        ? 0
        : uint8((q.usedBytes * 100) / q.quotaLimitBytes);
    q.lastUpdated = block.timestamp;

    emit QuotaRefunded(userAddress, fileSizeBytes);
}

Enforcement mode

The ENFORCE_QUOTA_ON_UPLOAD environment variable controls what happens when the on-chain quota check fails during an upload:
.env
# true  → upload is blocked when the contract call fails or quota is exceeded
# false → upload continues with a warning (default)
ENFORCE_QUOTA_ON_UPLOAD=false

ENFORCE_QUOTA_ON_UPLOAD=true

The backend submits updateQuotaAfterUpload and treats a revert or error as a hard block. The upload is rejected and the file is not pinned to IPFS. Use this in production when quota enforcement must be guaranteed.

ENFORCE_QUOTA_ON_UPLOAD=false

The backend attempts updateQuotaAfterUpload but continues if the call fails or the quota is not active. A warning is logged. Use this in development or when the contract is not yet deployed.
When ENFORCE_QUOTA_ON_UPLOAD=false and USE_REAL_CONTRACTS=false (mock mode), no quota check is performed at all. Files are uploaded without any on-chain limit. This is the default out-of-the-box behavior.

Querying quota via the API

GET /blockchain/quota returns the on-chain quota state for the authenticated user’s wallet address. Authentication is required — the endpoint reads the wallet address from the session. Response in real contract mode (USE_REAL_CONTRACTS=true):
{
  "walletAddress": "0xYourWalletAddress",
  "mode": "real",
  "tier": "CUSTOM",
  "quotaLimitBytes": "5368709120",
  "usedBytes": "1073741824",
  "remainingBytes": "4294967296",
  "usagePercent": 20
}
Response in mock mode (USE_REAL_CONTRACTS=false):
{
  "walletAddress": "0xYourWalletAddress",
  "mode": "mock",
  "quotaLimitBytes": null,
  "usedBytes": null,
  "remainingBytes": null,
  "usagePercent": null
}
The tier field is only present in real mode. The quotaLimitBytes, usedBytes, and remainingBytes fields are returned as strings because Ethereum uint256 values exceed JavaScript’s safe integer range — parse them with BigInt when doing arithmetic on the client side.
The remainingBytes field is computed on-chain by getQuotaStats as quotaLimitBytes - usedBytes, clamped to zero. It is not derived client-side.

The getQuotaStats function

The GET /blockchain/quota endpoint calls getQuotaStats on the contract, which returns the full quota state in a single view call:
contracts/BlockchainDriveUnified.sol
function getQuotaStats(address userAddress)
    external
    view
    returns (
        string memory tier,
        uint256 quotaLimitBytes,
        uint256 usedBytes,
        uint256 remainingBytes,
        uint8 usagePercent,
        uint256 filesUploaded,
        uint256 maxFiles,
        bool isActive,
        uint256 lastUpdated
    )
This is a view function — it reads state only and costs no gas. The backend accesses return values by named field when ethers v6 populates them, with positional index fallback:
routes/blockchainRoutes.js
const stats = await blockchainService.storageContract.getQuotaStats(walletAddress);
res.json({
  walletAddress,
  mode: "real",
  tier: stats.tier ?? stats[0],
  quotaLimitBytes: (stats.quotaLimitBytes ?? stats[1])?.toString?.() ?? String(stats[1]),
  usedBytes: (stats.usedBytes ?? stats[2])?.toString?.() ?? String(stats[2]),
  remainingBytes: (stats.remainingBytes ?? stats[3])?.toString?.() ?? String(stats[3]),
  usagePercent: Number(stats.usagePercent ?? stats[4])
});

Drive-level quota

In addition to the on-chain quota, Blockchain Drive enforces a per-drive byte limit stored in the MySQL database. Both limits are evaluated independently during an upload — if either is exceeded, the upload is blocked.

On-chain quota

Stored in the UserQuota struct on the contract. Scoped to a wallet address across all drives. Enforced by updateQuotaAfterUpload.

Drive-level quota

Stored in the database alongside the drive record. Scoped to a single drive. Enforced by the Express upload handler before the IPFS pin and contract call.
A user who is within their on-chain quota but has filled a specific drive’s storage limit will still be blocked from uploading to that drive. Similarly, a user whose on-chain quota is exhausted cannot upload to any drive regardless of the individual drive limit.

Quota transfer between users

The quotaService.js service exposes transferQuota(fromUserId, toUserId, amountBytes), which redistributes allocated quota between two users without involving the smart contract. It reads both users’ current quota snapshots from the database, verifies the sender has sufficient remaining quota, then calls assignRoleAndQuota to deduct from the sender and add to the receiver:
services/quotaService.js
export async function transferQuota(fromUserId, toUserId, amountBytes) {
  const sender = await getQuotaSnapshot(fromUserId);
  const receiver = await getQuotaSnapshot(toUserId);

  if (sender.remainingBytes < amountBytes) {
    const err = new Error("Insufficient remaining quota to share with user");
    err.code = "QUOTA_EXCEEDED";
    throw err;
  }

  await assignRoleAndQuota(fromUserId, sender.role, sender.quotaBytes - amountBytes);
  await assignRoleAndQuota(toUserId, receiver.role, receiver.quotaBytes + amountBytes);
}
transferQuota operates on the database quota snapshot, not the on-chain UserQuota struct. If you also want the transferred amount reflected on-chain, issue a separate allocateUserQuota transaction for each affected address.

Smart contract reference

Full function signatures, events, and deployment steps for BlockchainDriveUnified.

Architecture overview

How IPFS, Ethereum, and the Express backend interact end-to-end.

Build docs developers (and LLMs) love