Skip to content

Commit

Permalink
Controlling deposit/withdraw policy by the admin
Browse files Browse the repository at this point in the history
So the users won't troll
  • Loading branch information
KnightChaser committed Jun 8, 2024
1 parent 3413228 commit da6a6d2
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 4 deletions.
8 changes: 7 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ SQLALCHEMY_DB_SQLITE3_PATH="sqlite:///kcx.db"
TEST_ACCOUNT_ID="test"
TEST_ACCOUNT_PW="test"
TEST_ACCOUNT_EMAIL="[email protected]"
STARTING_BALANCE_IN_KRW=20000000000 # 20 billion KRW

# An example SECRET KEY for JWT. Change this to a random in production!
JWT_SECRET_KEY="KCXU$3R$3CR3T4JWT_"
Expand All @@ -12,4 +13,9 @@ JWT_TOKEN_EXPIRES_MINUTES=360 # 6 hours
REDIS_PORT=6379
REDIS_DB=0
REDIS_UDPATE_INTERVAL_IN_SECONDS=1
USER_RANKING_UPDATE_INTERVAL_IN_SECONDS=10
USER_RANKING_UPDATE_INTERVAL_IN_SECONDS=10

# Service configuration
# - Money balance (Disable(false) this if you want to use a fixed starting balance for all users)
ALLOW_ARBITRARY_BALANCE_DEPOSIT=false
ALLOW_ARBITRARY_BALANCE_WITHDRAW=false
45 changes: 44 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,63 @@
# KCX
> **K**nightchaser's **C**ryptocurrency e**X**change
## TECH STACK
- **Service buildup**

![sveltekit_lgo](https://img.shields.io/badge/SvelteKit-FF3E00?style=for-the-badge&logo=Svelte&logoColor=white)
![vite_logo](https://img.shields.io/badge/Vite-B73BFE?style=for-the-badge&logo=vite&logoColor=FFD62E)
![nodejs_logo](https://img.shields.io/badge/Node%20js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)
![JWT](https://img.shields.io/badge/JWT-black?style=for-the-badge&logo=JSON%20web%20tokens)
![fastapi_logo](https://img.shields.io/badge/fastapi-109989?style=for-the-badge&logo=FASTAPI&logoColor=white)
![sqlite_logo](https://img.shields.io/badge/Sqlite-003B57?style=for-the-badge&logo=sqlite&logoColor=white)
![redis_logo](https://img.shields.io/badge/redis-%23DD0031.svg?&style=for-the-badge&logo=redis&logoColor=white)
![docker-logo](https://img.shields.io/badge/Docker-2CA5E0?style=for-the-badge&logo=docker&logoColor=white)
![nginx_logo](https://img.shields.io/badge/Nginx-009639?style=for-the-badge&logo=nginx&logoColor=white)

- **Deployment**

![docker-logo](https://img.shields.io/badge/Docker-2CA5E0?style=for-the-badge&logo=docker&logoColor=white)
![aws_logo](https://img.shields.io/badge/Amazon_AWS-FF9900?style=for-the-badge&logo=amazonaws&logoColor=white)

## A Free-From-Risk cryptocurrency exchange simulation web application

You can try KCX at **[https://kcx.knightchaser.com/](https://kcx.knightchaser.com/)**, which is hosted via AWS Lightsail by the repository owner. Note that the specifications, service status, and other things might be changed depending on the development contexts and situations. (Try only for fun! :D)

## Service environmental variables
Currently, there are environment variables to set up the services as you need. Read the next chapter(`Deployment`) for complete contextual information.
```env
SQLALCHEMY_DB_SQLITE3_PATH="sqlite:///kcx.db"
TEST_ACCOUNT_ID="test"
TEST_ACCOUNT_PW="test"
TEST_ACCOUNT_EMAIL="[email protected]"
COMMON_STARTING_BALANCE_IN_KRW=20000000000 # 20 billion KRW
# An example SECRET KEY for JWT. Change this to a random in production!
JWT_SECRET_KEY="KCXU$3R$3CR3T4JWT_"
JWT_TOKEN_EXPIRES_MINUTES=360 # 6 hours
# A custom API server built with Redis
REDIS_PORT=6379
REDIS_DB=0
REDIS_UDPATE_INTERVAL_IN_SECONDS=1
USER_RANKING_UPDATE_INTERVAL_IN_SECONDS=10
# Service configuration
# - Money balance (Disable(false) this if you want to use a fixed starting balance for all users)
ALLOW_ARBITRARY_BALANCE_DEPOSIT=false
ALLOW_ARBITRARY_BALANCE_WITHDRAW=false
```
- Permanent data for this service will be stored in SQLite3. Configure `SQLALCHEMY_DB_SQLITE3_PATH` for the specified path.
- As a default, there are default account settings.
- The service will create a default account for testing when the service starts. (Will not if it's already created)
- Configure `TEST_ACCOUNT_ID`(ID = username), `TEST_ACCOUNT_PW`(password), and `TEST_ACCOUNT_EMAIL`(email) for the test account.
- `COMMON_STARTING_BALANCE_IN_KRW` defines how much fund will be initially granted to the new user.
- This server uses JWT for user authentication. Configure `JWT_SECRET_KEY` for JWT server key(change to another random value or your own) and `JWT_TOKEN_EXPIRES_MINUTES` for JWT expiary period.
- For the price information, this service uses the market data provided from UpBIT. Of course this also mimics the cryptocurrency exchange, there are a lot of requests about the crypto market data(Generally 3 to 5 requests per second per connected user) To reduce the direct API request to UpBIT, this use REDIS database as a cache. The server caches the market data every second and multiple connected users obtain the data from this REDIS cache.
- `REDIS_UPDATE_INTERVAL_IN_SECONDS` means which second this service refreshes the market data from UpBIT to the REDIS cache periodically. Basically, you can safely request up to 5 requests per second to UpBIT according to the current policy.
- `USER_RANKING_UPDATE_INTERVAL_IN_SECONDS` means which second this service calculate the users' ranking data according to the calculated estimated total asset value periodically (for leaderboard). Note that this ranking calculation relatively takes a lot of computational loads(iterating all users and calculating all types of assets for estimation), so don't make it too short.
- `ALLOW_ARBITRARY_BALANCE_*` configures whether the user controls their virtual balances on their own. If these are set true, then they can unlimitedly deposit(increase) and withdraw(decrease) the wallet, that may impact on the leaderboard. If you run this service for competitions or some rules, set it to false so no one except for the administrator can control the user's balances. (By accessing the SQLite3 database.)

## Deployment
- Clone the repository on your server/environments
```sh
Expand Down
9 changes: 9 additions & 0 deletions api/user/balance.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from sqlalchemy.orm import Session
from models import DepositWithdrawHistory, TradeHistory
from schemas import BalanceSchema, BalanceDepositWithdrawSchema
import os
from .authentication import get_current_user
from .credentials import get_user_id_by_username

Expand Down Expand Up @@ -35,6 +36,10 @@ def get_balance(db:Session = Depends(get_sqlite3_db), current_user:User = Depend
# Deposit money(KRW; fiat currency) to the user's account
@router.post("/api/account/deposit/KRW", response_model=BalanceDepositWithdrawSchema)
def deposit_KRW(deposit_balance: BalanceDepositWithdrawSchema, db: Session = Depends(get_sqlite3_db), current_user: User = Depends(get_current_user)) -> BalanceDepositWithdrawSchema:
# Check if the deposit feature is allowed in the environment
if os.getenv("ALLOW_ARBITRARY_BALANCE_DEPOSIT", "false").lower() != "true":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Deposit is not allowed in the current environment. Contact the administrator.")

# Deposit can't be a negative number
if deposit_balance.KRW <= 0:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Deposit amount must be greater than zero")
Expand Down Expand Up @@ -64,6 +69,10 @@ def deposit_KRW(deposit_balance: BalanceDepositWithdrawSchema, db: Session = Dep
# Withdraw money(KRW; fiat currency) from the user's account
@router.post("/api/account/withdraw/KRW", response_model=BalanceDepositWithdrawSchema)
def withdraw_KRW(withdraw_balance: BalanceDepositWithdrawSchema, db: Session = Depends(get_sqlite3_db), current_user: User = Depends(get_current_user)) -> BalanceDepositWithdrawSchema:
# Check if the withdraw feature is allowed in the environment
if os.getenv("ALLOW_ARBITRARY_BALANCE_WITHDRAW", "false").lower() != "true":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Withdraw is not allowed in the current environment. Contact the administrator.")

# Withdraw can't be a negative number
if withdraw_balance.KRW <= 0:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Withdraw amount must be greater than zero")
Expand Down
4 changes: 2 additions & 2 deletions api/user/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,11 @@ def register(user: UserRegistrationSchema, db: Session = Depends(get_sqlite3_db)
db.commit()
db.refresh(new_user)

# Registration successful, charge initial fund 1,000,000 KRW to the user's account
# Registration successful, charge initial fund as
# Note that the initial fund is only for demonstration purposes
new_balance = Balance(
user_id=new_user.id,
KRW=1000000
KRW=int(os.getenv("STARTING_BALANCE_IN_KRW", 1000000))
)
db.add(new_balance)
db.commit()
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/routes/user/functions/deposit.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ export async function depositKRW() {

return response.data;
} catch (error) {
// Deposit was forbidden (403) due to the administrator's policy.
if (error.response && error.response.status === 403) {
Swal.showValidationMessage(
"Deposit is forbidden due to the administrator's policy. Contact the administrator for more information."
);
return;
}

// Show an error message if the response is not OK
Swal.showValidationMessage(
`Request failed: ${error.response ? error.response.data : error.message}`
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/routes/user/functions/withdraw.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ export async function withdrawKRW() {

return response.data;
} catch (error) {
// Withdrawal was forbidden (403) due to the administrator's policy.
if (error.response && error.response.status === 403) {
Swal.showValidationMessage(
"Withdrawal is forbidden due to the administrator's policy. Contact the administrator for more information."
);
return;
}

// Throw an error if the response is not OK
Swal.showValidationMessage(
`Request failed: ${error.response ? error.response.data : error.message}`
Expand Down
10 changes: 10 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ async def lifespan(app: FastAPI):
# Load environment variables. So, we can use them in the application-wide
load_dotenv()

# Register service environment variables
ALLOW_ARBITRARY_BALANCE_DEPOSIT = os.getenv("ALLOW_ARBITRARY_BALANCE_DEPOSIT", "false").lower() == "true" # Can user deposit any amount of money?
console.log(f"Allow arbitrary balance deposit: {ALLOW_ARBITRARY_BALANCE_DEPOSIT}")

ALLOW_ARBITRARY_BALANCE_WITHDRAW = os.getenv("ALLOW_ARBITRARY_BALANCE_WITHDRAW", "false").lower() == "true" # Can user withdraw any amount of money?
console.log(f"Allow arbitrary balance withdraw: {ALLOW_ARBITRARY_BALANCE_WITHDRAW}")

COMMON_STARTING_BALANCE_IN_KRW = int(os.getenv("COMMON_STARTING_BALANCE_IN_KRW", 1000000)) # Common starting balance for all users
console.log(f"Common starting balance in KRW: {COMMON_STARTING_BALANCE_IN_KRW}")

# Check if the current environment is in Docker
is_docker = os.getenv("IS_IN_KCX_BACKEND_DOCKER", "false").lower() == "true"
console.log(f"Running in Docker: {is_docker}")
Expand Down

0 comments on commit da6a6d2

Please sign in to comment.