diff --git a/lending-app/README.md b/lending-app/README.md new file mode 100644 index 0000000..bc3295f --- /dev/null +++ b/lending-app/README.md @@ -0,0 +1,76 @@ +# Lending Protocol on CCF Framework + +## Overview +This is a decentralized lending protocol built on the Microsoft CCF framework. The protocol enables: +- **Collateralized Lending**: Users can deposit BTC or ETH as collateral and borrow USDT. +- **Liquidity Pool Management**: Users can add or remove liquidity from BTC and ETH pools. +- **Interest Distribution**: Liquidity providers earn DEFI tokens as interest rewards. + +--- + +## Features +- **Collateral Management**: + - Deposit BTC or ETH as collateral to borrow USDT. + - Collateral factor applied for safe borrowing limits. + +- **Liquidity Pools**: + - BTC and ETH pools for providing liquidity. + - Interest distributed to liquidity providers in DEFI tokens. + +- **Wallet Operations**: + - Deposit and withdraw tokens. + - Query wallet balances. + +--- + +## API Endpoints +### Wallet Endpoints +- **Deposit Token**: `/wallet/deposit` +- **Withdraw Token**: `/wallet/withdraw` +- **Get Wallet Balance**: `/wallet/balance` + +### Lending Endpoints +- **Add Collateral**: `/lending/collateral/add` +- **Borrow Tokens**: `/lending/borrow` +- **Remove Liquidity**: `/lending/liquidity/remove` + +--- + +## Configuration +- **Collateral Factor**: Default `1.5` (borrow limit = collateral / collateralFactor). +- **Base Interest Rate**: Default `2%` per period. + +--- + +## Example Usage +### Add Collateral +**Request**: +```json +POST /lending/collateral/add +{ + "userId": "user1", + "symbol": "BTC", + "amount": 2 +} + +{ + "statusCode": 200, + "body": "Added 2 of token BTC as collateral for user1." +} +``` +### Borrow Against Collateral +**Request**: +```json +POST /lending/borrow +{ + "userId": "user1", + "borrowAmount": 1000, + "baseTokenSymbol": "USDT" +} + +{ + "statusCode": 200, + "body": "Borrowed 1000 of token USDT against collateral." +} + +``` \ No newline at end of file diff --git a/lending-app/package-lock.json b/lending-app/package-lock.json new file mode 100644 index 0000000..358e17a --- /dev/null +++ b/lending-app/package-lock.json @@ -0,0 +1,55 @@ +{ + "name": "lending-app", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@microsoft/ccf-app": "^6.0.0-dev11", + "typescript": "^5.7.3" + }, + "devDependencies": { + "@types/node": "^22.10.5" + } + }, + "node_modules/@microsoft/ccf-app": { + "version": "6.0.0-dev11", + "resolved": "https://registry.npmjs.org/@microsoft/ccf-app/-/ccf-app-6.0.0-dev11.tgz", + "integrity": "sha512-eT8PVXDiG2WntMLDtq/7rgphykhTTVTIWQ6iXw+oTmviCdhsFdPIfRsjdDABd+lhF22KZ5ujUYsD4Hzp74o3lQ==", + "license": "Apache-2.0", + "bin": { + "ccf-build-bundle": "scripts/build_bundle.js" + } + }, + "node_modules/@types/node": { + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/lending-app/package.json b/lending-app/package.json new file mode 100644 index 0000000..4702ab0 --- /dev/null +++ b/lending-app/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "@microsoft/ccf-app": "^6.0.0-dev11", + "typescript": "^5.7.3" + }, + "devDependencies": { + "@types/node": "^22.10.5" + } +} diff --git a/lending-app/src/endpoints/lending.ts b/lending-app/src/endpoints/lending.ts new file mode 100644 index 0000000..04d73f2 --- /dev/null +++ b/lending-app/src/endpoints/lending.ts @@ -0,0 +1,154 @@ +import * as ccfapp from "@microsoft/ccf-app"; +import { addCollateral, borrow } from "../modules/lending"; +import { handleError } from "../utils/errorHandler"; + + +/** + * Handles depositing tokens into the lending protocol for collateral. + * + * This endpoint allows users to directly add tokens to their collateral, + * deducting the specified amount from their wallet balance. The collateral + * is used to increase the user's borrowing capacity. + * + * Input: + * - `userId` (string): The unique identifier for the user making the deposit. + * - `symbol` (string): The token symbol (e.g., "DEFI"). + * - `amount` (number): The amount of the token to deposit as collateral. + * + * Output: + * - Success: Returns a success message confirming the collateral addition. + * - Failure: Returns an error message with the appropriate HTTP status code. + * + * Workflow: + * 1. Parse and validate the input parameters (`userId`, `symbol`, `amount`). + * 2. Check that the user exists and has sufficient balance for the token. + * 3. Deduct the specified `amount` of `symbol` tokens from the user's wallet. + * 4. Add the deducted tokens to the user's collateral in the lending protocol. + * 5. Return a success message confirming the operation. + * + * Example Request: + * ```json + * { + * "userId": "user1", + * "symbol": "DEFI", + * "amount": 500 + * } + * ``` + * + * Example Response (Success): + * ```json + * { + * "statusCode": 200, + * "body": "Added 500 of token DEFI as collateral for user1." + * } + * ``` + * + * Example Response (Error: Invalid Input): + * ```json + * { + * "statusCode": 400, + * "body": "Invalid input parameters" + * } + * ``` + * + * Example Response (Error: Insufficient Balance): + * ```json + * { + * "statusCode": 400, + * "body": "Insufficient token balance for collateral addition." + * } + * ``` + */ +export function addCollateralEndpoint(request: ccfapp.Request): ccfapp.Response { + let body; + try { + body = request.body.json(); + } catch { + return { statusCode: 400, body: "Invalid request body" }; + } + + const { userId, symbol, amount } = body; + + // Validate input parameters + if (!userId || !symbol || amount <= 0) { + return { statusCode: 400, body: "Invalid input parameters" }; + } + + try { + // Add tokens directly to collateral + const collateralResult = addCollateral(userId, symbol, amount); + + return { statusCode: 200, body: collateralResult }; + } catch (error) { + return handleError(error); + } +} + +/** + * Handles borrowing tokens against collateral in the lending protocol. + * + * This endpoint validates the user's collateral and borrowing capacity + * before crediting the borrowed tokens to their wallet. The borrow + * operation deducts the borrowed amount from the protocol's liquidity + * pool and increases the user's debt balance. + * + * Input: + * - `userId` (string): The unique identifier of the user making the request. + * - `borrowAmount` (number): The amount of the token to borrow. + * - `baseTokenSymbol` (string): The symbol of the token to borrow (e.g., "BTC"). + * + * Output: + * - Success: Returns a success message confirming the borrowed amount. + * - Failure: Returns an error message with the appropriate HTTP status code. + * + * Workflow: + * 1. Validates input parameters (`userId`, `borrowAmount`, `baseTokenSymbol`). + * 2. Calls the `borrow` method in the lending module to: + * - Verify collateral sufficiency. + * - Deduct the borrowed tokens from the liquidity pool. + * - Increase the user's debt. + * - Credit the borrowed tokens to the user's wallet. + * 3. Returns a response with the result of the borrowing operation. + * + * + * Example Request: + * ```json + * { + * "userId": "user1", + * "borrowAmount": 150, + * "baseTokenSymbol": "BTC" + * } + * ``` + * + * Example Response: + * ```json + * { + * "statusCode": 200, + * "body": "Borrowed 150 of token BTC against collateral." + * } + * ``` + */ +export function borrowEndpoint(request: ccfapp.Request): ccfapp.Response { + let body; + try { + body = request.body.json(); + } catch { + return { statusCode: 400, body: "Invalid request body" }; + } + + const { userId, borrowAmount, baseTokenSymbol } = body; + + // Validate input parameters + if (!userId || !baseTokenSymbol || borrowAmount <= 0) { + return { statusCode: 400, body: "Invalid input parameters" }; + } + + try { + // Attempt to borrow tokens using the lending logic + const result = borrow(userId, borrowAmount, baseTokenSymbol); + + return { statusCode: 200, body: result }; + } catch (error) { + return handleError(error); + } +} diff --git a/lending-app/src/endpoints/liquidityPools.ts b/lending-app/src/endpoints/liquidityPools.ts new file mode 100644 index 0000000..1616bc0 --- /dev/null +++ b/lending-app/src/endpoints/liquidityPools.ts @@ -0,0 +1,165 @@ +import * as ccfapp from "@microsoft/ccf-app"; +import { addLiquidity, removeLiquidity } from "../modules/liquidityPools"; +import { handleError } from "../utils/errorHandler"; + +/** + * Handles depositing tokens into the lending protocol as collateral. + * + * This endpoint allows users to deposit tokens into the lending protocol + * to be used as collateral for borrowing other tokens. The collateral + * increases the user's borrowing capacity and is deducted from their wallet. + * + * Liquidity Pools: + * - Supported pools for collateral are `BTC` and `ETH`. + * + * Input: + * - `userId` (string): The unique identifier for the user making the deposit. + * - `symbol` (string): The token symbol to deposit as collateral (e.g., "BTC", "ETH"). + * - `amount` (number): The amount of the token to deposit. + * + * Output: + * - Success: Returns a success message confirming the collateral addition. + * - Failure: Returns an error message with the appropriate HTTP status code. + * + * Workflow: + * 1. Validate the input parameters (`userId`, `symbol`, `amount`). + * 2. Deduct the specified token amount from the user's wallet. + * 3. Add the deducted tokens to the user's collateral balance. + * 4. Update the account and collateral records in the protocol. + * + * Example Request: + * ```json + * { + * "userId": "user1", + * "symbol": "BTC", + * "amount": 2 + * } + * ``` + * + * Example Response (Success): + * ```json + * { + * "statusCode": 200, + * "body": "Added 2 of token BTC as collateral for user1." + * } + * ``` + * + * Example Response (Error: Invalid Input): + * ```json + * { + * "statusCode": 400, + * "body": "Invalid input parameters" + * } + * ``` + */ +export function addLiquidityEndpoint(request: ccfapp.Request): ccfapp.Response { + let body; + try { + body = request.body.json(); + } catch { + return { statusCode: 400, body: "Invalid request body" }; + } + + const { userId, symbol, amount } = body; + + if (!userId || !symbol || amount <= 0) { + return { statusCode: 400, body: "Invalid input parameters" }; + } + + try { + const result = addLiquidity(userId, symbol, amount); + return { statusCode: 200, body: result }; + } catch (error) { + return handleError(error); + } +} + + +/** + * Handles the removal of liquidity from the protocol's liquidity pools. + * + * This endpoint allows users to withdraw their previously supplied liquidity + * from the protocol. The tokens are deducted from the pool's total liquidity + * and returned to the user's wallet. Liquidity providers may also forfeit any + * accrued interest when withdrawing their contribution. + * + * Liquidity Pools: + * - Supported pools for liquidity are `BTC` and `ETH`. + * + * Input: + * - `userId` (string): The unique identifier of the user requesting the withdrawal. + * - `symbol` (string): The token symbol of the pool to withdraw liquidity from (e.g., "BTC", "ETH"). + * - `amount` (number): The amount of tokens to withdraw from the liquidity pool. + * + * Output: + * - Success: Returns a success message confirming the withdrawal. + * - Failure: Returns an error message with the appropriate HTTP status code. + * + * Workflow: + * 1. Validate the input parameters (`userId`, `symbol`, `amount`). + * 2. Retrieve the user's contribution to the specified liquidity pool. + * 3. Verify that the user has sufficient liquidity in the pool to fulfill the withdrawal request. + * 4. Deduct the requested amount from the pool's total liquidity. + * 5. Credit the withdrawn tokens to the user's wallet. + * 6. Update the pool and user records in the protocol. + * 7. Return a success message confirming the withdrawal. + * + * Example Request: + * ```json + * { + * "userId": "user1", + * "symbol": "BTC", + * "amount": 1.5 + * } + * ``` + * + * Example Response (Success): + * ```json + * { + * "statusCode": 200, + * "body": "Removed 1.5 of token BTC from the liquidity pool for user1." + * } + * ``` + * + * Example Response (Error: Insufficient Liquidity): + * ```json + * { + * "statusCode": 400, + * "body": "Insufficient liquidity in the user's contribution to the BTC pool." + * } + * ``` + * + * Example Response (Error: Invalid Input): + * ```json + * { + * "statusCode": 400, + * "body": "Invalid input parameters" + * } + * ``` + * + * Notes: + * - The protocol ensures that the user's withdrawal request does not exceed their contribution to the specified pool. + * - Withdrawing liquidity may impact the interest earnings of the user, as interest is proportional to the remaining contribution. + * - This operation reduces the total liquidity available in the specified pool, affecting other borrowers and liquidity providers. + */ +export function removeLiquidityEndpoint(request: ccfapp.Request): ccfapp.Response { + let body; + try { + body = request.body.json(); + } catch { + return { statusCode: 400, body: "Invalid request body" }; + } + + const { userId, symbol, amount } = body; + + if (!userId || !symbol || amount <= 0) { + return { statusCode: 400, body: "Invalid input parameters" }; + } + + try { + const result = removeLiquidity(userId, symbol, amount); + return { statusCode: 200, body: result }; + } catch (error) { + return handleError(error); + } +} diff --git a/lending-app/src/endpoints/wallet.ts b/lending-app/src/endpoints/wallet.ts new file mode 100644 index 0000000..6f48773 --- /dev/null +++ b/lending-app/src/endpoints/wallet.ts @@ -0,0 +1,208 @@ +import * as ccfapp from "@microsoft/ccf-app"; +import { depositToken, withdrawToken } from "../modules/wallet"; +import { getOrCreateAccount } from "../modules/accounts"; +import { handleError } from "../utils/errorHandler"; + +/** + * Handles depositing tokens into a user's wallet. + * + * This endpoint allows tokens to be added to a user's wallet balance. It is typically + * used to credit tokens after borrowing, interest accrual, or rewards distribution. + * + * Input: + * - `userId` (string): The unique identifier for the user receiving the tokens. + * - `token` (string): The token symbol to deposit (e.g., "USDT", "DEFI"). + * - `amount` (number): The amount of tokens to deposit into the wallet. + * + * Output: + * - Success: Returns a success message confirming the deposit. + * - Failure: Returns an error message with the appropriate HTTP status code. + * + * Workflow: + * 1. Validate the input parameters (`userId`, `token`, `amount`). + * 2. Add the specified token and amount to the user's wallet balance. + * 3. Update the user's account in the account table. + * 4. Return a success message. + * + * Example Request: + * ```json + * { + * "userId": "user1", + * "token": "USDT", + * "amount": 100 + * } + * ``` + * + * Example Response (Success): + * ```json + * { + * "statusCode": 200, + * "body": "Deposited 100 of token USDT into user1's wallet." + * } + * ``` + * + * Example Response (Error: Invalid Input): + * ```json + * { + * "statusCode": 400, + * "body": "Invalid input parameters" + * } + * ``` + */ +export function depositTokenEndpoint(request: ccfapp.Request): ccfapp.Response { + let body; + try { + body = request.body.json(); + } catch { + return { statusCode: 400, body: "Invalid request body" }; + } + + const { userId, token, amount } = body; + + // Validate input parameters + if (!userId || !token || amount <= 0) { + return { statusCode: 400, body: "Invalid input parameters" }; + } + + try { + const result = depositToken(userId, token, amount); + return { statusCode: 200, body: result }; + } catch (error) { + return handleError(error); + } +} + +/** + * Handles withdrawing tokens from a user's wallet. + * + * This endpoint allows tokens to be deducted from a user's wallet balance. It is typically + * used when users deposit collateral, repay loans, or transfer tokens. + * + * Input: + * - `userId` (string): The unique identifier for the user. + * - `token` (string): The token symbol to withdraw (e.g., "USDT", "BTC"). + * - `amount` (number): The amount of tokens to withdraw from the wallet. + * + * Output: + * - Success: Returns a success message confirming the withdrawal. + * - Failure: Returns an error message with the appropriate HTTP status code. + * + * Workflow: + * 1. Validate the input parameters (`userId`, `token`, `amount`). + * 2. Deduct the specified token and amount from the user's wallet balance. + * 3. Update the user's account in the account table. + * 4. Return a success message. + * + * Example Request: + * ```json + * { + * "userId": "user1", + * "token": "USDT", + * "amount": 50 + * } + * ``` + * + * Example Response (Success): + * ```json + * { + * "statusCode": 200, + * "body": "Withdrew 50 of token USDT from user1's wallet." + * } + * ``` + * + * Example Response (Error: Insufficient Balance): + * ```json + * { + * "statusCode": 400, + * "body": "Insufficient balance in the wallet for token USDT." + * } + * ``` + */ +export function withdrawTokenEndpoint(request: ccfapp.Request): ccfapp.Response { + let body; + try { + body = request.body.json(); + } catch { + return { statusCode: 400, body: "Invalid request body" }; + } + + const { userId, token, amount } = body; + + // Validate input parameters + if (!userId || !token || amount <= 0) { + return { statusCode: 400, body: "Invalid input parameters" }; + } + + try { + const result = withdrawToken(userId, token, amount); + return { statusCode: 200, body: result }; + } catch (error) { + return handleError(error); + } +} + +/** + * Retrieves the wallet balance for a specific token and user. + * + * This endpoint allows users or external systems to query the balance of a specific + * token in the user's wallet. + * + * Input: + * - `userId` (string): The unique identifier for the user. + * - `token` (string): The token symbol to query (e.g., "BTC", "ETH"). + * + * Output: + * - Success: Returns the wallet balance for the specified token. + * - Failure: Returns an error message with the appropriate HTTP status code. + * + * Workflow: + * 1. Validate the input parameters (`userId`, `token`). + * 2. Retrieve the user's account and wallet balance for the specified token. + * 3. Return the balance. + * + * Example Request: + * ``` + * GET /wallet/balance?user_id=user1&token=BTC + * ``` + * + * Example Response (Success): + * ```json + * { + * "statusCode": 200, + * "body": { + * "walletBalance": 150 + * } + * } + * ``` + * + * Example Response (Error: Invalid Input): + * ```json + * { + * "statusCode": 400, + * "body": "User ID is required" + * } + * ``` + */ +export function getWalletBalanceEndpoint( + request: ccfapp.Request, +): ccfapp.Response { + const userId = request.params.user_id; + const token = request.params.token; + + // Validate input parameters + if (!userId) { + return { statusCode: 400, body: "User ID is required" }; + } + + if (!token) { + return { statusCode: 400, body: "Token is required" }; + } + + // Retrieve the user's account and balance + const account = getOrCreateAccount(userId); + + return { + statusCode: 200, + body: { walletBalance: account.balances[token] || 0 }, + }; +} \ No newline at end of file diff --git a/lending-app/src/index.ts b/lending-app/src/index.ts new file mode 100644 index 0000000..210bd1a --- /dev/null +++ b/lending-app/src/index.ts @@ -0,0 +1,4 @@ +import "./endpoints/lending"; +import "./endpoints/wallet"; + +console.log("DeFi like Lending Sample ..."); \ No newline at end of file diff --git a/lending-app/src/modules/accounts.ts b/lending-app/src/modules/accounts.ts new file mode 100644 index 0000000..ed7a4ee --- /dev/null +++ b/lending-app/src/modules/accounts.ts @@ -0,0 +1,27 @@ +import * as ccfapp from "@microsoft/ccf-app"; + +export interface Account { + balances: Record; // Token balances keyed by token address + collateral: Record; // Total collateral locked + debt: number; // Total debt +} + +export const accountTable = ccfapp.typedKv( + "accounts", + ccfapp.string, + ccfapp.json(), +); + +export function getOrCreateAccount(userId: string): Account { + if (!accountTable.has(userId)) { + const newAccount: Account = { + balances: {}, // Initialize empty balances + collateral: {}, + debt: 0, + }; + accountTable.set(userId, newAccount); + return newAccount; + } + + return accountTable.get(userId) as Account; +} diff --git a/lending-app/src/modules/interest.ts b/lending-app/src/modules/interest.ts new file mode 100644 index 0000000..9a01133 --- /dev/null +++ b/lending-app/src/modules/interest.ts @@ -0,0 +1,44 @@ +import { accountTable, getOrCreateAccount } from "./accounts"; +import { poolTable } from "./liquidityPools"; +import { logTransaction } from "./transactions"; + +const BASE_RATE = 0.02; + +export function accrueInterest(): void { + const pool = poolTable.get("defaultPool"); + if (!pool) { + console.error("Liquidity pool does not exist."); + return; + } + + // Iterate over all tokens in the pool + for (const token in pool.totalLiquidity) { + const totalLiquidity = pool.totalLiquidity[token]; + + if (totalLiquidity > 0) { + // Calculate total interest for the pool + const totalInterest = totalLiquidity * BASE_RATE; + + // Distribute interest to all contributors + for (const userId in pool.contributions[token]) { + const userContribution = pool.contributions[token][userId]; + const userInterest = (userContribution / totalLiquidity) * totalInterest; + + // Add interest to the user's balance + const account = getOrCreateAccount(userId); + account.balances[token] = (account.balances[token] || 0) + userInterest; + accountTable.set(userId, account); + + logTransaction("accrueInterest", userId, userInterest); + + console.log(`Accrued ${userInterest} of token ${token} to user ${userId}`); + } + + // Deduct total interest from the protocol's total liquidity + pool.totalLiquidity[token] -= totalInterest; + } + } + + // Update the pool in the table + poolTable.set("defaultPool", pool); +} diff --git a/lending-app/src/modules/lending.ts b/lending-app/src/modules/lending.ts new file mode 100644 index 0000000..12d3bd9 --- /dev/null +++ b/lending-app/src/modules/lending.ts @@ -0,0 +1,60 @@ +import { accountTable, getOrCreateAccount } from "./accounts"; +import { logTransaction } from "./transactions"; +import { depositToken } from "./wallet"; +import { poolTable } from "./liquidityPools"; + +const COLLATERAL_FACTOR = 1.5; + +export function addCollateral(userId: string, symbol: string, amount: number): string { + const account = getOrCreateAccount(userId); + + if ((account.balances[symbol] || 0) < amount) { + throw new Error("Insufficient token balance."); + } + + account.balances[symbol] -= amount; + account.collateral[symbol] = (account.collateral[symbol] || 0) + amount; + accountTable.set(userId, account); + + logTransaction("addCollateral", userId, amount); + + return `Added ${amount} of token ${symbol} as collateral for ${userId}.`; +} + +export function borrow(userId: string, borrowAmount: number, baseTokenSymbol: string): string { + const account = getOrCreateAccount(userId); + + // Calculate total collateral value with collateral factor applied + const totalCollateral = Object.values(account.collateral).reduce((sum, amount) => sum + amount, 0); + const maxBorrowable = totalCollateral / COLLATERAL_FACTOR; + + if (borrowAmount > maxBorrowable) { + throw new Error("Insufficient collateral to borrow this amount."); + } + + // Deduct the borrowed amount from the protocol's total supply + // Step 2: Access the liquidity pool and check total supply + const pool = poolTable.get("defaultPool"); + if (!pool || (pool.totalLiquidity[baseTokenSymbol] || 0) < borrowAmount) { + throw new Error(`Insufficient liquidity in the pool for token ${baseTokenSymbol}.`); + } + + // Step 3: Withdraw the borrowed amount from the liquidity pool + pool.totalLiquidity[baseTokenSymbol] -= borrowAmount; + poolTable.set("defaultPool", pool); + + // Credit the borrowed amount to the user's wallet + depositToken(userId, baseTokenSymbol, borrowAmount); + + // Increase the user's debt + account.debt += borrowAmount; + + // Update the user's account in the account table + accountTable.set(userId, account); + + // Log the borrowing transaction + logTransaction("borrow", userId, borrowAmount); + + return `Borrowed ${borrowAmount} of token ${baseTokenSymbol} against collateral.`; +} + diff --git a/lending-app/src/modules/liquidityPools.ts b/lending-app/src/modules/liquidityPools.ts new file mode 100644 index 0000000..1fdf418 --- /dev/null +++ b/lending-app/src/modules/liquidityPools.ts @@ -0,0 +1,76 @@ +import * as ccfapp from "@microsoft/ccf-app"; +import { accountTable, getOrCreateAccount } from "./accounts"; +import { logTransaction } from "./transactions"; + +export interface Pool { + totalLiquidity: Record; // Total liquidity for each token + contributions: Record>; // User contributions per token + } + + +export const poolTable = ccfapp.typedKv( + "liquidityPools", + ccfapp.string, // Key: pool identifier (e.g., "defaultPool") + ccfapp.json(), // Value: Pool object +); + +export function addLiquidity(userId: string, symbol: string, amount: number): string { + const account = getOrCreateAccount(userId); + + if ((account.balances[symbol] || 0) < amount) { + throw new Error("Insufficient token balance."); + } + + // Get or create the pool + const pool = poolTable.get("defaultPool") || { totalLiquidity: {}, contributions: {} }; + + // Update the user's contribution + pool.contributions[symbol] = pool.contributions[symbol] || {}; + pool.contributions[symbol][userId] = (pool.contributions[symbol][userId] || 0) + amount; + + // Update total liquidity + pool.totalLiquidity[symbol] = (pool.totalLiquidity[symbol] || 0) + amount; + + // Deduct the tokens from the user's wallet + account.balances[symbol] -= amount; + poolTable.set("defaultPool", pool); + + // Update the account in the table + accountTable.set(userId, account); + + logTransaction("addLiquidity", userId, amount); + + return `Added ${amount} of token ${symbol} to the liquidity pool.`; +} + +export function removeLiquidity(userId: string, symbol: string, amount: number): string { + const account = getOrCreateAccount(userId); + + // Get the pool + const pool = poolTable.get("defaultPool"); + if (!pool || !pool.contributions[symbol] || (pool.contributions[symbol][userId] || 0) < amount) { + throw new Error("Insufficient liquidity in the pool."); + } + + // Update the user's contribution + pool.contributions[symbol][userId] -= amount; + + // Remove the entry if the contribution is now zero + if (pool.contributions[symbol][userId] === 0) { + delete pool.contributions[symbol][userId]; + } + + // Update total liquidity + pool.totalLiquidity[symbol] -= amount; + + // Credit the tokens back to the user's wallet + account.balances[symbol] = (account.balances[symbol] || 0) + amount; + + poolTable.set("defaultPool", pool); + accountTable.set(userId, account); + + logTransaction("removeLiquidity", userId, amount); + + return `Removed ${amount} of token ${symbol} from the liquidity pool.`; + } + \ No newline at end of file diff --git a/lending-app/src/modules/token.ts b/lending-app/src/modules/token.ts new file mode 100644 index 0000000..e5bb985 --- /dev/null +++ b/lending-app/src/modules/token.ts @@ -0,0 +1,28 @@ + +import * as ccfapp from "@microsoft/ccf-app"; + + +export interface Token { + name: string; + symbol: string; + totalSupply: number; +} + +export const tokenTable = ccfapp.typedKv( + "tokens", + ccfapp.string, // Key: token symbol + ccfapp.json(), // Value: Token object +); + + +export function createToken(symbol: string, name: string, totalSupply: number): string { + if (tokenTable.has(symbol)) { + throw new Error(`Token with symbol ${symbol} already exists.`); + } + + const token: Token = { name, symbol, totalSupply }; + tokenTable.set(symbol, token); + + return `Token ${name} (${symbol}) created with total supply of ${totalSupply}`; + } + \ No newline at end of file diff --git a/lending-app/src/modules/transactions.ts b/lending-app/src/modules/transactions.ts new file mode 100644 index 0000000..6fc5b9b --- /dev/null +++ b/lending-app/src/modules/transactions.ts @@ -0,0 +1,25 @@ +import * as ccfapp from "@microsoft/ccf-app"; + +export interface Transaction { + action: string; + user: string; + amount: number; + timestamp: string; +} + +export const transactionTable = ccfapp.typedKv( + "transactions", + ccfapp.string, + ccfapp.json(), +); + +export function logTransaction(action: string, user: string, amount: number) { + const transactionId = `${Date.now()}-${user}`; + const transaction: Transaction = { + action, + user, + amount, + timestamp: new Date().toISOString(), + }; + transactionTable.set(transactionId, transaction); +} \ No newline at end of file diff --git a/lending-app/src/modules/wallet.ts b/lending-app/src/modules/wallet.ts new file mode 100644 index 0000000..6c40239 --- /dev/null +++ b/lending-app/src/modules/wallet.ts @@ -0,0 +1,44 @@ +import { Account, getOrCreateAccount } from "./accounts"; +import { logTransaction } from "./transactions"; + +export function depositToken(userId: string, token: string, amount: number): string { + const account = getOrCreateAccount(userId); + + const balance = getTokenBalance(account, token); + updateTokenBalance(account, token, balance + amount); + + logTransaction("depositToken", userId, amount); + + return `Deposited ${amount} of token ${token} to ${userId}'s wallet.`; +} + + +export function withdrawToken(userId: string, token: string, amount: number): string { + const account = getOrCreateAccount(userId); + + const balance = getTokenBalance(account, token); + + if (balance < amount) { + throw new Error(`Insufficient balance for token ${token}`); + } + + // Deduct the amount and update the balance + updateTokenBalance(account, token, balance - amount); + + // Log the transaction + logTransaction("withdrawToken", userId, amount); + + return `Withdrawn ${amount} of token ${token} from ${userId}'s wallet.`; +} + +function getTokenBalance(account: Account, token: string): number { + return account.balances[token] || 0; +} + +function updateTokenBalance(account: Account, token: string, amount: number): void { + if (amount === 0) { + delete account.balances[token]; + } else { + account.balances[token] = amount; + } +} diff --git a/lending-app/src/utils/errorHandler.ts b/lending-app/src/utils/errorHandler.ts new file mode 100644 index 0000000..69f7c22 --- /dev/null +++ b/lending-app/src/utils/errorHandler.ts @@ -0,0 +1,10 @@ +import * as ccfapp from "@microsoft/ccf-app"; + + +export function handleError(error: unknown): ccfapp.Response { + if (error instanceof Error) { + return { statusCode: 400, body: error.message }; + } else { + return { statusCode: 400, body: "An unknown error occurred." }; + } +} \ No newline at end of file diff --git a/lending-app/tsconfig.json b/lending-app/tsconfig.json new file mode 100644 index 0000000..d916a2c --- /dev/null +++ b/lending-app/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file