clone:legacy

This commit is contained in:
2025-09-05 11:09:58 +09:00
commit 6103518feb
119 changed files with 41713 additions and 0 deletions

25
.dockerignore Normal file
View File

@@ -0,0 +1,25 @@
node_modules
dist
coverage
.git
.gitignore
Dockerfile*
docker-compose*.yml
.env
npm-debug.log*
yarn-error.log*
.pnpm-store
.vscode
.idea
.cache
*.log
bkon-wallet-firebase-adminsdk-*.json
*.md
**/*.md
**/*.spec.ts
**/__tests__
**/__snapshots__
**/.husky
**/.github
**/.gitlab
**/*.dbml

3
.env.ci.example Normal file
View File

@@ -0,0 +1,3 @@
DATABASE_URL=mysql://root:root@127.0.0.1:3306/test
REDIS_HOST=127.0.0.1
REDIS_PORT=6379

23
.env.development Normal file
View File

@@ -0,0 +1,23 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="mysql://MNCO:1q2w3e4r!@localhost:3306/MNCO"
PORT=4000
project_id = bkon-wallet
private_key_id = cee0810b562b74af99270e39b3f60b4376a72575
private_key = -----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCy92XIau//rj/k\nM0ruoaRYnllBJMKBb4MvtrEzYE2Oc3hTqVwmdZz7jiOiqHcRzR7R8ilVbjQE5BXD\nTZ+vSKR12jaOlrNEM1uOSLqDVup6kOPFCnayFBdf2YZicnrqiLlzEW2u+ZZaJS85\nLzxnrlV2zwKxGBYWL9QSoMQWhAWnKJp07BPYO6xV9TVgwD0N8kKGHUUC6eL8Z+hU\nhpekA/s4YWPIDr4wBHlhGFdC9i52nSgJ10lqAJF6iwMan1Dl+HTMHZAvihS4DCEV\nIZ/lcBdW9x51JS+J4/gm/MGIBXy9971nIGEjf4MlmV0a1q49e9nylTBuOE6nUIIU\ngTN/GjdhAgMBAAECggEABQekKMTCpTG5R3lek4Q0/thHzhIm/6V0mdhytKzco9KL\nJkogq3m2QAavvzWoumDEteYDszbjR44+g1bw6ZJV1vRwq0Nr2ztDSTrc4m3vWKMD\nrR5yaruwUtK7BBTPb2czfnBWCsB72+GRfnQH/f+oahaphB8sbb1fN+fPTbUHsKSM\n4QbBDRAtuOnMFsZCri4bMYGKcgghog9Kxm1V1mLWZHMmpR2ZFKvVb8VAe+Zco4Ju\nVV5RxM+3okl6owcJJK2xGoFMcST26qqg/HyAuPx5aXyyxloAji3R6bm0UL/HrfHI\njSOXbzqso7pK8sjmbzwVlzT14UjyYPXWJy3/Yf9mcQKBgQDbd/lGTFooGgjeuijO\nWMME807UbP8IH8JoiCvlHg8eqZfLCRHjdWDKB98zIZvCkxRAEhKSHR6T95EGv9mO\nVVOYC/DiyTVcSv8Vt8gTvZhHsJdGN5QAo4cACNt/JEvFxmpDeX84k4Y8JqY1jh3I\nQYL6XfduZiuShVbVNIe7xO68CQKBgQDQwYloAg5qYa2h6cENqF6eEmLHaxxYfW21\nNTEpavsE8IL0ouzYASzHgYao5f417xWGBb0HkcxlrkcrCpC8hixqSrLr84HNGuR8\nVRfe8j+ODdV4onSToLDVks28H9FUO5pVQiXPXr8ArsmkjngT0bfIaRhUyhMbANru\nn9ulhg6mmQKBgD9YbZagyxTwDsdarBSDAicXoxUlMKdDo3VQeHr1JiAPi0SLJaKl\nan5lr0Ku3KpYkWu8y6doyD6lIjL0hPLUJgCo0apjsQcmjmHSXel0u9NVYRRfTlSw\n3nJgHBqie0xmbJ11IAdQbVpHPYoPrwDyB8AEBzrSOplb6yg2tUa5HL8hAoGARoD2\n3U/Eep1evQ5riydQPWbMQbmlKyXBha/fWLOu764jLGhSQWm0K/VM+4Ih5ylGRatu\nej39oGHJ23mIBIP0QDnWT+Y/8nugq3U5yKxcVqfJbyK+6JUe5CLepSjB1AcFSsI6\nbtz6+UoPBCqx10+/GEqWUxykczxItMr8rdym2hECgYEAnaiCviw3db3izsg5D6Tn\nCiV7NDDQLKu5eGd18DjlnHmGklqeK+huhXnzGT3pOihfmJ4Pa03c8hcqeKZifdk1\nIBc01PylXM3W1i1IsXNHUKzpPapafGAMYiNbiQpBsA4jK87srbQfx9PEzZTHrLaD\niz/Qfcewwssu7XgFFg5gyIE=\n-----END PRIVATE KEY-----\n
client_email = firebase-adminsdk-i39eg@bkon-wallet.iam.gserviceaccount.com
client_id = 117190562737897069953
auth_uri = https://accounts.google.com/o/oauth2/auth
token_uri = https://oauth2.googleapis.com/token
auth_provider_x509_cert_url = https://www.googleapis.com/oauth2/v1/certs
client_x509_cert_url = https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-i39eg%40bkon-wallet.iam.gserviceaccount.com
IAM_ACCESS_KEY=AKIAT4QQBTECUELRRMXX
IAM_SECRET_KEY=Ee2g2MrPQzBoVePepj2TqwFATuVOf6ACe7RpQCnd

26
.env.docker.example Normal file
View File

@@ -0,0 +1,26 @@
# Use this to run via docker-compose
PORT=4000
NODE_ENV=development
# DB inside compose
DATABASE_URL=mysql://MNCO:MNCO@mysql:3306/MNCO
MYSQL_ROOT_PASSWORD=root
MYSQL_DATABASE=MNCO
MYSQL_USER=MNCO
MYSQL_PASSWORD=MNCO
# Redis inside compose
REDIS_HOST=redis
REDIS_PORT=6379
# Firebase (optional in dev)
project_id=
private_key_id=
private_key=
client_email=
client_id=
auth_uri=
token_uri=
auth_provider_x509_cert_url=
client_x509_cert_url=
firebase_url=

2
.env.dockerize.example Normal file
View File

@@ -0,0 +1,2 @@
USER=
TOKEN=

29
.env.example Normal file
View File

@@ -0,0 +1,29 @@
# App
PORT=4000
NODE_ENV=development
# Database (MySQL)
DATABASE_URL=mysql://bkon:bkon@mysql:3306/bkon
MYSQL_ROOT_PASSWORD=root
MYSQL_DATABASE=bkon
MYSQL_USER=bkon
MYSQL_PASSWORD=bkon
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
# JWT (move secrets to real .env)
JWT_SECRET=change-me
# Firebase service account (paste values without quotes)
project_id=
private_key_id=
private_key=
client_email=
client_id=
auth_uri=
token_uri=
auth_provider_x509_cert_url=
client_x509_cert_url=
firebase_url=

23
.env.production.example Normal file
View File

@@ -0,0 +1,23 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="mysql://MNCO:MNCO@172.30.1.34:3306/MNCO"
PORT=4000
project_id = bkon-wallet
private_key_id = cee0810b562b74af99270e39b3f60b4376a72575
private_key = -----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCy92XIau//rj/k\nM0ruoaRYnllBJMKBb4MvtrEzYE2Oc3hTqVwmdZz7jiOiqHcRzR7R8ilVbjQE5BXD\nTZ+vSKR12jaOlrNEM1uOSLqDVup6kOPFCnayFBdf2YZicnrqiLlzEW2u+ZZaJS85\nLzxnrlV2zwKxGBYWL9QSoMQWhAWnKJp07BPYO6xV9TVgwD0N8kKGHUUC6eL8Z+hU\nhpekA/s4YWPIDr4wBHlhGFdC9i52nSgJ10lqAJF6iwMan1Dl+HTMHZAvihS4DCEV\nIZ/lcBdW9x51JS+J4/gm/MGIBXy9971nIGEjf4MlmV0a1q49e9nylTBuOE6nUIIU\ngTN/GjdhAgMBAAECggEABQekKMTCpTG5R3lek4Q0/thHzhIm/6V0mdhytKzco9KL\nJkogq3m2QAavvzWoumDEteYDszbjR44+g1bw6ZJV1vRwq0Nr2ztDSTrc4m3vWKMD\nrR5yaruwUtK7BBTPb2czfnBWCsB72+GRfnQH/f+oahaphB8sbb1fN+fPTbUHsKSM\n4QbBDRAtuOnMFsZCri4bMYGKcgghog9Kxm1V1mLWZHMmpR2ZFKvVb8VAe+Zco4Ju\nVV5RxM+3okl6owcJJK2xGoFMcST26qqg/HyAuPx5aXyyxloAji3R6bm0UL/HrfHI\njSOXbzqso7pK8sjmbzwVlzT14UjyYPXWJy3/Yf9mcQKBgQDbd/lGTFooGgjeuijO\nWMME807UbP8IH8JoiCvlHg8eqZfLCRHjdWDKB98zIZvCkxRAEhKSHR6T95EGv9mO\nVVOYC/DiyTVcSv8Vt8gTvZhHsJdGN5QAo4cACNt/JEvFxmpDeX84k4Y8JqY1jh3I\nQYL6XfduZiuShVbVNIe7xO68CQKBgQDQwYloAg5qYa2h6cENqF6eEmLHaxxYfW21\nNTEpavsE8IL0ouzYASzHgYao5f417xWGBb0HkcxlrkcrCpC8hixqSrLr84HNGuR8\nVRfe8j+ODdV4onSToLDVks28H9FUO5pVQiXPXr8ArsmkjngT0bfIaRhUyhMbANru\nn9ulhg6mmQKBgD9YbZagyxTwDsdarBSDAicXoxUlMKdDo3VQeHr1JiAPi0SLJaKl\nan5lr0Ku3KpYkWu8y6doyD6lIjL0hPLUJgCo0apjsQcmjmHSXel0u9NVYRRfTlSw\n3nJgHBqie0xmbJ11IAdQbVpHPYoPrwDyB8AEBzrSOplb6yg2tUa5HL8hAoGARoD2\n3U/Eep1evQ5riydQPWbMQbmlKyXBha/fWLOu764jLGhSQWm0K/VM+4Ih5ylGRatu\nej39oGHJ23mIBIP0QDnWT+Y/8nugq3U5yKxcVqfJbyK+6JUe5CLepSjB1AcFSsI6\nbtz6+UoPBCqx10+/GEqWUxykczxItMr8rdym2hECgYEAnaiCviw3db3izsg5D6Tn\nCiV7NDDQLKu5eGd18DjlnHmGklqeK+huhXnzGT3pOihfmJ4Pa03c8hcqeKZifdk1\nIBc01PylXM3W1i1IsXNHUKzpPapafGAMYiNbiQpBsA4jK87srbQfx9PEzZTHrLaD\niz/Qfcewwssu7XgFFg5gyIE=\n-----END PRIVATE KEY-----\n
client_email = firebase-adminsdk-i39eg@bkon-wallet.iam.gserviceaccount.com
client_id = 117190562737897069953
auth_uri = https://accounts.google.com/o/oauth2/auth
token_uri = https://oauth2.googleapis.com/token
auth_provider_x509_cert_url = https://www.googleapis.com/oauth2/v1/certs
client_x509_cert_url = https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-i39eg%40bkon-wallet.iam.gserviceaccount.com
IAM_ACCESS_KEY=AKIAT4QQBTECUELRRMXX
IAM_SECRET_KEY=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

23
.env.production.gkod Normal file
View File

@@ -0,0 +1,23 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="mysql://gkodadmin:1q2w3e4r!@gkod-prod-database.cwlpwodonnzc.ap-southeast-1.rds.amazonaws.com:3306/GKOD"
PORT=4000
project_id = bkon-wallet
private_key_id = cee0810b562b74af99270e39b3f60b4376a72575
private_key = -----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCy92XIau//rj/k\nM0ruoaRYnllBJMKBb4MvtrEzYE2Oc3hTqVwmdZz7jiOiqHcRzR7R8ilVbjQE5BXD\nTZ+vSKR12jaOlrNEM1uOSLqDVup6kOPFCnayFBdf2YZicnrqiLlzEW2u+ZZaJS85\nLzxnrlV2zwKxGBYWL9QSoMQWhAWnKJp07BPYO6xV9TVgwD0N8kKGHUUC6eL8Z+hU\nhpekA/s4YWPIDr4wBHlhGFdC9i52nSgJ10lqAJF6iwMan1Dl+HTMHZAvihS4DCEV\nIZ/lcBdW9x51JS+J4/gm/MGIBXy9971nIGEjf4MlmV0a1q49e9nylTBuOE6nUIIU\ngTN/GjdhAgMBAAECggEABQekKMTCpTG5R3lek4Q0/thHzhIm/6V0mdhytKzco9KL\nJkogq3m2QAavvzWoumDEteYDszbjR44+g1bw6ZJV1vRwq0Nr2ztDSTrc4m3vWKMD\nrR5yaruwUtK7BBTPb2czfnBWCsB72+GRfnQH/f+oahaphB8sbb1fN+fPTbUHsKSM\n4QbBDRAtuOnMFsZCri4bMYGKcgghog9Kxm1V1mLWZHMmpR2ZFKvVb8VAe+Zco4Ju\nVV5RxM+3okl6owcJJK2xGoFMcST26qqg/HyAuPx5aXyyxloAji3R6bm0UL/HrfHI\njSOXbzqso7pK8sjmbzwVlzT14UjyYPXWJy3/Yf9mcQKBgQDbd/lGTFooGgjeuijO\nWMME807UbP8IH8JoiCvlHg8eqZfLCRHjdWDKB98zIZvCkxRAEhKSHR6T95EGv9mO\nVVOYC/DiyTVcSv8Vt8gTvZhHsJdGN5QAo4cACNt/JEvFxmpDeX84k4Y8JqY1jh3I\nQYL6XfduZiuShVbVNIe7xO68CQKBgQDQwYloAg5qYa2h6cENqF6eEmLHaxxYfW21\nNTEpavsE8IL0ouzYASzHgYao5f417xWGBb0HkcxlrkcrCpC8hixqSrLr84HNGuR8\nVRfe8j+ODdV4onSToLDVks28H9FUO5pVQiXPXr8ArsmkjngT0bfIaRhUyhMbANru\nn9ulhg6mmQKBgD9YbZagyxTwDsdarBSDAicXoxUlMKdDo3VQeHr1JiAPi0SLJaKl\nan5lr0Ku3KpYkWu8y6doyD6lIjL0hPLUJgCo0apjsQcmjmHSXel0u9NVYRRfTlSw\n3nJgHBqie0xmbJ11IAdQbVpHPYoPrwDyB8AEBzrSOplb6yg2tUa5HL8hAoGARoD2\n3U/Eep1evQ5riydQPWbMQbmlKyXBha/fWLOu764jLGhSQWm0K/VM+4Ih5ylGRatu\nej39oGHJ23mIBIP0QDnWT+Y/8nugq3U5yKxcVqfJbyK+6JUe5CLepSjB1AcFSsI6\nbtz6+UoPBCqx10+/GEqWUxykczxItMr8rdym2hECgYEAnaiCviw3db3izsg5D6Tn\nCiV7NDDQLKu5eGd18DjlnHmGklqeK+huhXnzGT3pOihfmJ4Pa03c8hcqeKZifdk1\nIBc01PylXM3W1i1IsXNHUKzpPapafGAMYiNbiQpBsA4jK87srbQfx9PEzZTHrLaD\niz/Qfcewwssu7XgFFg5gyIE=\n-----END PRIVATE KEY-----\n
client_email = firebase-adminsdk-i39eg@bkon-wallet.iam.gserviceaccount.com
client_id = 117190562737897069953
auth_uri = https://accounts.google.com/o/oauth2/auth
token_uri = https://oauth2.googleapis.com/token
auth_provider_x509_cert_url = https://www.googleapis.com/oauth2/v1/certs
client_x509_cert_url = https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-i39eg%40bkon-wallet.iam.gserviceaccount.com
IAM_ACCESS_KEY=AKIAT4QQBTECUELRRMXX
IAM_SECRET_KEY=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

39
.gitignore vendored Normal file
View File

@@ -0,0 +1,39 @@
# compiled output
/dist
/node_modules
commit-sha.txt
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.env
.adminjs

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
pnpm lint-staged

16
.prettierignore Normal file
View File

@@ -0,0 +1,16 @@
# Ignore build output
node_modules
build
dist
coverage
.next
out
# Logs
*.log
# Prisma generated
prisma/migrations
# Artifacts
*.min.*

10
.prettierrc.json Normal file
View File

@@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"printWidth": 80,
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"plugins": ["prettier-plugin-prisma"],
"arrowParens": "always"
}

0
.vscode/settings.json vendored Normal file
View File

96
Dockerfile Normal file
View File

@@ -0,0 +1,96 @@
# syntax=docker/dockerfile:1.7
# -------- Base image --------
FROM node:22-bookworm-slim AS base
WORKDIR /app
# Enable corepack for pnpm/yarn
RUN corepack enable
# Install OS deps
RUN apt-get update && apt-get install -y --no-install-recommends openssl ca-certificates && rm -rf /var/lib/apt/lists/*
# -------- Install all dependencies (dev) --------
FROM base AS deps
# Copy only manifest files for better caching
COPY package.json pnpm-lock.yaml* yarn.lock* package-lock.json* ./
# Install deps (prefer pnpm if lock exists)
RUN if [ -f pnpm-lock.yaml ]; then corepack prepare pnpm@latest --activate && pnpm install --frozen-lockfile; \
elif [ -f yarn.lock ]; then yarn install --frozen-lockfile; \
else npm ci; fi
# -------- Build source --------
FROM base AS build
ENV NODE_ENV=development
COPY --from=deps /app/node_modules ./node_modules
COPY . .
COPY prisma ./prisma
# Generate Prisma client and build
RUN npx prisma generate
RUN if [ -f pnpm-lock.yaml ]; then pnpm build; \
elif [ -f yarn.lock ]; then yarn build; \
else npm run build; fi
# -------- Production dependencies only --------
FROM base AS prod-deps
ENV NODE_ENV=production \
HUSKY=0
COPY package.json pnpm-lock.yaml* yarn.lock* package-lock.json* ./
RUN if [ -f pnpm-lock.yaml ]; then corepack prepare pnpm@latest --activate && pnpm install --frozen-lockfile --ignore-scripts; \
elif [ -f yarn.lock ]; then yarn install --frozen-lockfile --production=true --ignore-scripts; \
else npm ci --omit=dev --ignore-scripts; fi
# -------- Runtime image --------
FROM base AS runner
ARG PORT=4000
ENV NODE_ENV=production \
HUSKY=0 \
PORT=${PORT}
WORKDIR /app
# Non-root user
RUN groupadd -g 1001 nodejs && useradd -u 1001 -g nodejs -s /bin/bash -m nest
# Copy runtime artifacts
COPY ./commit-sha.txt ./
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/package.json ./
COPY --from=build /app/commit-sha.txt ./
COPY --from=build /app/ecosystem.config.js ./
COPY --from=build /app/dist ./dist
COPY --from=build /app/static ./static
COPY --from=build /app/prisma ./prisma
# you need this for components
COPY --from=build /app/src/components ./src/components
# Remove unnecessary sources from runtime image to reduce size
# (src is not required at runtime; prisma schema is needed for generate only)
# Keep only what start:prod needs
# Generate Prisma client in runtime stage to ensure correct paths
RUN npx prisma generate
# Create adminjs directory and set proper ownership for the nest user
RUN mkdir -p .adminjs && chown -R 1001:1001 /app/.adminjs
USER 1001
EXPOSE ${PORT}
CMD ["npm", "run", "start:prod"]
# -------- Migrate image (one-shot) --------
FROM base AS migrate
ENV NODE_ENV=production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Generate prisma client (safe even if already generated)
RUN npx prisma generate
# Default command runs migrations against DATABASE_URL
CMD ["npx", "prisma", "migrate", "deploy"]
# -------- Dev image (watch mode) --------
FROM deps AS dev
ARG PORT=4000
ENV NODE_ENV=development \
PORT=${PORT}
WORKDIR /app
COPY . .
RUN npx prisma generate
EXPOSE ${PORT}
CMD ["npm", "run", "start:dev"]

73
README.md Normal file
View File

@@ -0,0 +1,73 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ yarn install
```
## Running the app
```bash
# development
$ yarn run start
# watch mode
$ yarn run start:dev
# production mode
$ yarn run start:prod
```
## Test
```bash
# unit tests
$ yarn run test
# e2e tests
$ yarn run test:e2e
# test coverage
$ yarn run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

3
bin/updateCommitSha.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
git rev-parse --short=6 HEAD > commit-sha.txt

View File

@@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "bkon-wallet",
"private_key_id": "cee0810b562b74af99270e39b3f60b4376a72575",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCy92XIau//rj/k\nM0ruoaRYnllBJMKBb4MvtrEzYE2Oc3hTqVwmdZz7jiOiqHcRzR7R8ilVbjQE5BXD\nTZ+vSKR12jaOlrNEM1uOSLqDVup6kOPFCnayFBdf2YZicnrqiLlzEW2u+ZZaJS85\nLzxnrlV2zwKxGBYWL9QSoMQWhAWnKJp07BPYO6xV9TVgwD0N8kKGHUUC6eL8Z+hU\nhpekA/s4YWPIDr4wBHlhGFdC9i52nSgJ10lqAJF6iwMan1Dl+HTMHZAvihS4DCEV\nIZ/lcBdW9x51JS+J4/gm/MGIBXy9971nIGEjf4MlmV0a1q49e9nylTBuOE6nUIIU\ngTN/GjdhAgMBAAECggEABQekKMTCpTG5R3lek4Q0/thHzhIm/6V0mdhytKzco9KL\nJkogq3m2QAavvzWoumDEteYDszbjR44+g1bw6ZJV1vRwq0Nr2ztDSTrc4m3vWKMD\nrR5yaruwUtK7BBTPb2czfnBWCsB72+GRfnQH/f+oahaphB8sbb1fN+fPTbUHsKSM\n4QbBDRAtuOnMFsZCri4bMYGKcgghog9Kxm1V1mLWZHMmpR2ZFKvVb8VAe+Zco4Ju\nVV5RxM+3okl6owcJJK2xGoFMcST26qqg/HyAuPx5aXyyxloAji3R6bm0UL/HrfHI\njSOXbzqso7pK8sjmbzwVlzT14UjyYPXWJy3/Yf9mcQKBgQDbd/lGTFooGgjeuijO\nWMME807UbP8IH8JoiCvlHg8eqZfLCRHjdWDKB98zIZvCkxRAEhKSHR6T95EGv9mO\nVVOYC/DiyTVcSv8Vt8gTvZhHsJdGN5QAo4cACNt/JEvFxmpDeX84k4Y8JqY1jh3I\nQYL6XfduZiuShVbVNIe7xO68CQKBgQDQwYloAg5qYa2h6cENqF6eEmLHaxxYfW21\nNTEpavsE8IL0ouzYASzHgYao5f417xWGBb0HkcxlrkcrCpC8hixqSrLr84HNGuR8\nVRfe8j+ODdV4onSToLDVks28H9FUO5pVQiXPXr8ArsmkjngT0bfIaRhUyhMbANru\nn9ulhg6mmQKBgD9YbZagyxTwDsdarBSDAicXoxUlMKdDo3VQeHr1JiAPi0SLJaKl\nan5lr0Ku3KpYkWu8y6doyD6lIjL0hPLUJgCo0apjsQcmjmHSXel0u9NVYRRfTlSw\n3nJgHBqie0xmbJ11IAdQbVpHPYoPrwDyB8AEBzrSOplb6yg2tUa5HL8hAoGARoD2\n3U/Eep1evQ5riydQPWbMQbmlKyXBha/fWLOu764jLGhSQWm0K/VM+4Ih5ylGRatu\nej39oGHJ23mIBIP0QDnWT+Y/8nugq3U5yKxcVqfJbyK+6JUe5CLepSjB1AcFSsI6\nbtz6+UoPBCqx10+/GEqWUxykczxItMr8rdym2hECgYEAnaiCviw3db3izsg5D6Tn\nCiV7NDDQLKu5eGd18DjlnHmGklqeK+huhXnzGT3pOihfmJ4Pa03c8hcqeKZifdk1\nIBc01PylXM3W1i1IsXNHUKzpPapafGAMYiNbiQpBsA4jK87srbQfx9PEzZTHrLaD\niz/Qfcewwssu7XgFFg5gyIE=\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-i39eg@bkon-wallet.iam.gserviceaccount.com",
"client_id": "117190562737897069953",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-i39eg%40bkon-wallet.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

49
compose.local.yml Normal file
View File

@@ -0,0 +1,49 @@
version: '3.9'
services:
api:
image: mnco-mobile-api:dev
build:
context: .
target: dev
env_file:
- .env.docker.example
environment:
- DATABASE_URL=mysql://bkon:bkon@mysql:3306/bkon
- REDIS_HOST=redis
- REDIS_PORT=6379
ports:
- '4000:4000'
volumes:
- .:/app
- /app/node_modules
depends_on:
- mysql
- redis
mysql:
image: mysql:8.3
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=bkon
- MYSQL_USER=bkon
- MYSQL_PASSWORD=bkon
ports:
- '3306:3306'
volumes:
- mysql_data:/var/lib/mysql
command:
[
'mysqld',
'--default-authentication-plugin=mysql_native_password',
'--character-set-server=utf8mb4',
'--collation-server=utf8mb4_unicode_ci',
]
redis:
image: redis:7-alpine
ports:
- '6379:6379'
volumes:
- redis_data:/data
volumes:
mysql_data:
redis_data:

36
deploy.yml Normal file
View File

@@ -0,0 +1,36 @@
name: BKON API Server Deploy
on:
push:
branches: ['main']
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: '20'
- name: Install and build
run: |
npm install
yarn build
# deploy:
# needs: [build]
# runs-on: ubuntu-latest
# steps:
# - name: Build and deploy
# run: |
# echo "$SSH_PEM_KEY" >> $HOME/key.pem
# chmod 400 $HOME/key.pem
# ssh -i $HOME/key.pem -o StrictHostKeyChecking=no ${SSH_USER}@${SSH_KNOWN_HOSTS} '~/script.sh'
# env:
# SSH_USER: ${{secrets.SSH_USER}}
# SSH_KNOWN_HOSTS: ${{secrets.SSH_KNOWN_HOSTS}}
# SSH_PEM_KEY: ${{secrets.SSH_PEM_KEY}}

18
depth.js Normal file
View File

@@ -0,0 +1,18 @@
const depth = (n) => {
if (n > 1) {
return {
children: {
include: depth(n - 1),
},
};
} else {
return {
children: true,
};
}
};
console.log(JSON.stringify(depth(1000)));
//console.log(JSON.stringify(depth(2)))
//console.log(JSON.stringify(depth(3)))
//console.log(JSON.stringify(depth(4)))

22
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,22 @@
services:
api:
image: git.mnco.dev/mnco/regecy-wallet-backend:latest
container_name: mnco-mobile-api
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
- REDIS_HOST=redis
- REDIS_PORT=6379
- PORT=4000
depends_on:
- redis
ports:
- '4000:4000'
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
redis_data:

54
docker-compose.yml Normal file
View File

@@ -0,0 +1,54 @@
services:
api:
build:
context: .
target: dev
image: mnco-mobile-api:dev
container_name: mnco-mobile-api-dev
env_file:
- .env
environment:
- DATABASE_URL=mysql://MNCO:MNCO@mysql:3306/MNCO
- REDIS_HOST=redis
- REDIS_PORT=6379
- PORT=4000
volumes:
- .:/app
- /app/node_modules
depends_on:
- mysql
- redis
ports:
- '4000:4000'
mysql:
image: mysql:8.3
container_name: mnco-mysql
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=mnco
- MYSQL_USER=mnco
- MYSQL_PASSWORD=mnco
ports:
- '3306:3306'
volumes:
- mysql_data:/var/lib/mysql
command:
[
'mysqld',
'--default-authentication-plugin=mysql_native_password',
'--character-set-server=utf8mb4',
'--collation-server=utf8mb4_unicode_ci',
]
redis:
image: redis:7-alpine
container_name: mnco-redis
ports:
- '6379:6379'
volumes:
- redis_data:/data
volumes:
mysql_data:
redis_data:

6
docker-daemon.json Normal file
View File

@@ -0,0 +1,6 @@
{
"max-concurrent-uploads": 2,
"max-concurrent-downloads": 3,
"features": { "containerd-snapshotter": true },
"log-level": "info"
}

39
dockerize.yml Normal file
View File

@@ -0,0 +1,39 @@
services: # docker compose 서비스 정의 루트
dind: # Docker-in-Docker 데몬 컨테이너
image: docker:24-dind # 도커 데몬이 포함된 공식 dind 이미지(v24)
privileged: true # 컨테이너 안에서 도커 데몬을 구동하려면 privileged 권한 필요
environment:
- DOCKER_TLS_CERTDIR= # dind의 TLS 인증서 디렉터리 비활성화(평문 2375 사용)
volumes:
- dind-cache:/var/lib/docker # 도커 레이어/이미지 캐시를 볼륨에 보존해 재빌드 가속
- ./docker-daemon.json:/etc/docker/daemon.json:ro # 도커 데몬 설정(동시 업로드 제한 등) 주입
healthcheck:
test: ['CMD-SHELL', 'docker info > /dev/null 2>&1 || exit 1'] # 데몬 준비 여부 검사
interval: 2s # 2초마다 헬스체크
timeout: 1s # 1초 넘기면 실패로 간주
retries: 30 # 최대 30회 재시도(약 60초 대기)
builder: # 빌드/푸시를 수행하는 도커 CLI 컨테이너
image: docker:24-cli # 도커 CLI만 포함된 경량 이미지(v24)
depends_on:
dind:
condition: service_healthy # dind 헬스체크 통과 후에만 시작
working_dir: /workspace # 작업 디렉터리(호스트의 리포지토리를 마운트)
volumes:
- .:/workspace # 현재 리포지토리를 컨테이너에 바인드 마운트
env_file:
- ./.env.dockerize # USER/TOKEN, TAG 등 환경변수 로드
environment:
- DOCKER_HOST=tcp://dind:2375 # dind 데몬에 접속하도록 설정
- DOCKER_BUILDKIT=1 # BuildKit 활성화(빠르고 캐시 효율적)
- REGISTRY=git.mnco.dev # 푸시할 레지스트리 호스트
- IMAGE=mnco/regecy-wallet-backend # 레포지토리/이미지 이름
- TAG=latest # 기본 태그
command: [
'sh',
'-lc',
'until docker version >/dev/null 2>&1; do echo ''waiting for dind...''; sleep 1; done; echo "$$TOKEN" | docker login $$REGISTRY -u $$USER --password-stdin && docker buildx build --platform linux/amd64 --target runner -t $$REGISTRY/$$IMAGE:$$TAG --provenance=false --sbom=false --push .',
] # dind 대기 → 레지스트리 로그인 → buildx 빌드/푸시
volumes:
dind-cache: # dind의 /var/lib/docker를 저장하는 네임드 볼륨

18
ecosystem.config.js Normal file
View File

@@ -0,0 +1,18 @@
module.exports = {
apps: [
{
name: 'api', // pm2 name
script: './dist/main.js', // // 앱 실행 스크립트
instances: 1, // 클러스터 모드 사용 시 생성할 인스턴스 수
exec_mode: 'cluster', // fork, cluster 모드 중 선택
merge_logs: true, // 클러스터 모드 사용 시 각 클러스터에서 생성되는 로그를 한 파일로 합쳐준다.
autorestart: true, // 프로세스 실패 시 자동으로 재시작할지 선택
watch: false, // 파일이 변경되었을 때 재시작 할지 선택
// max_memory_restart: "512M", // 프로그램의 메모리 크기가 일정 크기 이상이 되면 재시작한다.
log_date_format: 'YYYY-MM-DD HH:mm Z',
env: {
NODE_ENV: 'prod',
},
},
],
};

15
eslint.config.mjs Normal file
View File

@@ -0,0 +1,15 @@
import js from '@eslint/js';
import globals from 'globals';
import tseslint from 'typescript-eslint';
import { defineConfig } from 'eslint/config';
export default defineConfig([
{
files: ['**/*.{js,mjs,cjs,ts,mts,cts}'],
plugins: { js },
extends: ['js/recommended'],
languageOptions: { globals: globals.browser },
},
{ files: ['**/*.js'], languageOptions: { sourceType: 'script' } },
tseslint.configs.recommended,
]);

8
nest-cli.json Normal file
View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

140
package.json Normal file
View File

@@ -0,0 +1,140 @@
{
"name": "mnco-mobile-api",
"version": "0.0.2",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"engines": {
"node": ">=22.0.0 <23.0.0"
},
"scripts": {
"build": "nest build",
"format": "pnpm run format:all",
"start": "./bin/updateCommitSha.sh; nest start",
"start:dev": "./bin/updateCommitSha.sh; nest start --watch",
"dev": "pnpm start:dev",
"start:debug": "nest start --debug --watch",
"start:prod": "pm2-runtime ecosystem.config.js --only api",
"lint": "pnpm run lint:all",
"lint:all": "eslint . --ext .ts,.tsx,.js,.jsx --fix --config eslint.config.mjs",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"prepare": "husky",
"format:all": "prettier --write .",
"format:staged": "lint-staged"
},
"dependencies": {
"@adminjs/bundler": "^3.0.0",
"@adminjs/design-system": "^4.1.1",
"@adminjs/express": "^6.1.1",
"@adminjs/import-export": "3.0.0",
"@adminjs/logger": "5.0.1",
"@adminjs/nestjs": "^6.1.0",
"@adminjs/passwords": "4.0.0",
"@adminjs/prisma": "^5.0.4",
"@adminjs/themes": "^1.0.1",
"@adminjs/typeorm": "^5.0.1",
"@adminjs/upload": "4.0.2",
"@antv/g6": "^5.0.49",
"@nestjs/common": "^11.1.6",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.1.6",
"@nestjs/jwt": "^11.0.0",
"@nestjs/mongoose": "^11.0.3",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.6",
"@nestjs/schedule": "^6.0.0",
"@nestjs/serve-static": "^5.0.3",
"@nestjs/swagger": "^11.2.0",
"@prisma/client": "^6.14.0",
"@tiptap/core": "^2.26.1",
"@tiptap/pm": "^2.26.1",
"adminjs": "^7.8.17",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"dayjs": "^1.11.13",
"decimal.js": "^10.6.0",
"ethers": "^6.15.0",
"express": "^5.1.0",
"express-formidable": "^1.2.0",
"express-session": "^1.18.2",
"firebase-admin": "^13.4.0",
"ioredis": "^5.7.0",
"json-2-csv": "^5.5.9",
"mongoose": "^8.17.2",
"node-cron": "^4.2.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pm2": "^5.4.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"swagger-ui-express": "^5.0.1",
"tslib": "^2.8.1",
"uuid": "^11.1.0",
"waait": "^1.0.5"
},
"devDependencies": {
"@babel/plugin-syntax-import-assertions": "^7.27.1",
"@eslint/js": "^9.33.0",
"@nestjs/cli": "^11.0.10",
"@nestjs/schematics": "^11.0.7",
"@nestjs/testing": "^11.1.6",
"@types/express": "^5.0.3",
"@types/jest": "^30.0.0",
"@types/node": "^24.3.0",
"@types/passport": "^1.0.17",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.3",
"@typescript-eslint/eslint-plugin": "^8.40.0",
"@typescript-eslint/parser": "^8.40.0",
"eslint": "^9.33.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"globals": "^16.3.0",
"husky": "^9.1.7",
"jest": "^30.0.5",
"jiti": "^2.5.1",
"lint-staged": "^16.1.5",
"prettier": "^3.6.2",
"prisma": "^6.14.0",
"prettier-plugin-prisma": "^5.0.0",
"prisma-dbml-generator": "^0.12.0",
"source-map-support": "^0.5.21",
"supertest": "^7.1.4",
"ts-jest": "^29.4.1",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.9.2",
"typescript-eslint": "^8.40.0"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"prettier --write"
],
"*.{json,md,yml,yaml,css,scss,less,html,graphql,prisma}": "prettier --write"
}
}

22912
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

229
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,229 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["views"]
}
generator dbml {
provider = "prisma-dbml-generator"
output = "../src"
outputName = "schema.dbml"
projectDatabaseType = "mysql"
projectName = "BKON Wallet Schema"
projectNote = "BKON Wallet Distributor Table"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model Board {
id Int @id @default(autoincrement())
title String?
body String? @db.Text
imgUrl String?
count Int @default(0)
categoryId Int?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
link String?
category Category? @relation(fields: [categoryId], references: [id])
@@index([title], map: "title")
@@index([categoryId], map: "Board_categoryId_fkey")
}
model Category {
id Int @id @default(autoincrement())
name String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
Board Board[]
}
model Node {
id Int @id @default(autoincrement())
address String @unique
balance Decimal @default(0) @db.Decimal(65, 0)
stakedBalance Decimal @default(0) @db.Decimal(65, 0)
computingPower Decimal @default(0) @db.Decimal(65, 0)
parentId Int?
isActivated Boolean @default(false)
nickname String?
referral String @unique
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
userId String?
childrenAccumulatedBalance Decimal @default(0) @db.Decimal(65, 0)
childrenAccumulatedCount Decimal @default(0) @db.Decimal(65, 0)
childrenAccumulatedActiveCount Decimal @default(0) @db.Decimal(65, 0)
childrenAccumulatedDeactiveCount Decimal @default(0) @db.Decimal(65, 0)
childrenAccumulatedStakedBalance Decimal @default(0) @db.Decimal(65, 0)
childrenDepth Int @default(0)
depth Int @default(0)
depositedBalance Decimal @default(0) @db.Decimal(65, 0)
realName String?
parent Node? @relation("ParentChild", fields: [parentId], references: [id])
children Node[] @relation("ParentChild")
User User? @relation(fields: [userId], references: [userId])
@@index([parentId], map: "Node_parentId_fkey")
@@index([userId], map: "Node_userId_fkey")
}
model Txes {
id Int @id @default(autoincrement())
txid String
from String
to String
value Decimal @default(0) @db.Decimal(65, 0)
fee Decimal @default(0) @db.Decimal(65, 0)
internalFee Decimal @default(0) @db.Decimal(65, 0)
type String
blockTimestamp DateTime
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
}
model User {
userId String @id @unique @default(uuid())
IMEI String?
mac String?
nickname String?
fcmToken String?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
pushAllowAt DateTime?
language String?
Node Node[]
favoriteAddress favoriteAddress[]
userNotification userNotification[]
}
model favoriteAddress {
address String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
userId String
nickname String?
User User @relation(fields: [userId], references: [userId])
@@id([address, userId])
@@index([userId], map: "favoriteAddress_userId_fkey")
}
model userNotification {
id String @id @default(cuid())
title String
body String
data Json
userId String
type NotificationType
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
link String
user User @relation(fields: [userId], references: [userId])
@@index([userId], map: "userNotification_userId_fkey")
}
model userToAddressBackup {
userId String
address String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@id([userId, address])
@@index([userId], map: "userToAddressBackup_userId_fkey")
}
model System {
id Int @id @default(autoincrement())
key String @unique
value String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
}
model snapshot {
id Int @id
address String @unique
nickname String?
parentId Int?
stakedBalance Decimal? @db.Decimal(65, 0)
computingPower Decimal? @db.Decimal(65, 0)
childrenAccumulatedBalance Decimal? @db.Decimal(65, 0)
isActivated Boolean
balance Decimal? @db.Decimal(65, 0)
childrenAccumulatedCount Decimal? @db.Decimal(25, 4)
childrenAccumulatedDeactiveCount Decimal? @db.Decimal(65, 4)
childrenAccumulatedStakedBalance Decimal? @db.Decimal(65, 4)
Txid String?
isCPSent Boolean
isRankSent Boolean
isCPSendStarted Boolean
isRankSendStarted Boolean
}
model distribution {
id Int @id
nickname String?
address String?
parentId Int?
ranking BigInt?
stakedBalance Float?
computingPower Float?
childrenAccumulatedBalance Float?
stakedBalanceOrig Decimal? @db.Decimal(65, 0)
computingPowerOrig Decimal? @db.Decimal(65, 0)
childrenAccumulatedBalanceOrig Decimal? @db.Decimal(65, 0)
ranking_portion Decimal? @db.Decimal(25, 4)
staked_portion Decimal? @db.Decimal(65, 4)
computingPower_portion Decimal? @db.Decimal(65, 4)
childrenAccumulatedBalance_portion Decimal? @db.Decimal(65, 4)
ranking_estimated_dist Decimal? @db.Decimal(42, 16)
computingPower_estimated_dist Decimal? @db.Decimal(65, 16)
Txid String?
isCPSent Boolean
isRankSent Boolean
isCPSendStarted Boolean
isRankSendStarted Boolean
}
model LockedCoin {
id String @id @default(cuid())
address String
amount Decimal @default(0) @db.Decimal(65, 0)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
unlocked Boolean @default(false)
unlockAt DateTime?
reason String?
txId String?
sendedAt DateTime?
}
model TransferHistories {
idx BigInt @id @default(autoincrement()) @db.UnsignedBigInt
txhash String @db.VarChar(100)
from_address String @db.VarChar(100)
to_address String @db.VarChar(100)
amount Decimal @db.Decimal(65, 0)
memo String? @db.Text
timestamp DateTime @default(now()) @db.DateTime
createdAt DateTime @default(now()) @db.DateTime
log_index BigInt @db.UnsignedBigInt
blockNumber BigInt @db.UnsignedBigInt
@@unique([txhash, log_index, timestamp])
@@index([idx])
@@index([log_index], name: "log_index")
@@index([from_address, to_address], name: "idxAddress")
@@map("TransferHistories")
}
enum NotificationType {
SYSTEM
USER
TRANSACTION
}

View File

@@ -0,0 +1,91 @@
WITH `ranking_list` AS (
SELECT
`n`.`id` AS `id`,
`n`.`nickname` AS `nickname`,
`n`.`address` AS `address`,
`n`.`stakedBalance` AS `stakedBalance`,
`n`.`computingPower` AS `computingPower`,
`n`.`childrenAccumulatedBalance` AS `childrenAccumulatedBalance`,
rank() OVER (
ORDER BY
`n`.`stakedBalance`
) AS `ranking`
FROM
`DKON`.`Node` `n`
WHERE
(`n`.`stakedBalance` >= 200000000000000000000)
ORDER BY
`n`.`stakedBalance` DESC
)
SELECT
`ranking_list`.`id` AS `id`,
`ranking_list`.`nickname` AS `nickname`,
`ranking_list`.`address` AS `address`,
`ranking_list`.`ranking` AS `ranking`,
round((`ranking_list`.`stakedBalance` / pow(10, 18)), 4) AS `stakedBalance`,
round((`ranking_list`.`computingPower` / pow(10, 18)), 4) AS `computingPower`,
round(
(
`ranking_list`.`childrenAccumulatedBalance` / pow(10, 18)
),
4
) AS `childrenAccumulatedBalance`,
`ranking_list`.`stakedBalance` AS `stakedBalanceOrig`,
`ranking_list`.`computingPower` AS `computingPowerOrig`,
`ranking_list`.`childrenAccumulatedBalance` AS `childrenAccumulatedBalanceOrig`,
(
`ranking_list`.`ranking` / (
SELECT
sum(`ranking_list`.`ranking`)
FROM
`ranking_list`
)
) AS `ranking_portion`,
(
`ranking_list`.`stakedBalance` / (
SELECT
sum(`ranking_list`.`stakedBalance`)
FROM
`ranking_list`
)
) AS `staked_portion`,
(
`ranking_list`.`computingPower` / (
SELECT
sum(`ranking_list`.`computingPower`)
FROM
`ranking_list`
)
) AS `computingPower_portion`,
(
`ranking_list`.`childrenAccumulatedBalance` / (
SELECT
sum(`ranking_list`.`childrenAccumulatedBalance`)
FROM
`ranking_list`
)
) AS `childrenAccumulatedBalance_portion`,
(
(
`ranking_list`.`ranking` / (
SELECT
sum(`ranking_list`.`ranking`)
FROM
`ranking_list`
)
) * 66666.666666666666
) AS `ranking_estimated_dist`,
(
(
`ranking_list`.`computingPower` / (
SELECT
sum(`ranking_list`.`computingPower`)
FROM
`ranking_list`
)
) * 66666.666666666666
) AS `computingPower_estimated_dist`
FROM
`ranking_list`
GROUP BY
`ranking_list`.`address`

View File

@@ -0,0 +1,66 @@
WITH `ranking_list` AS (
SELECT
`n`.`id` AS `id`,
`n`.`nickname` AS `nickname`,
`n`.`address` AS `address`,
`n`.`stakedBalance` AS `stakedBalance`,
`n`.`computingPower` AS `computingPower`,
`n`.`childrenAccumulatedBalance` AS `childrenAccumulatedBalance`
FROM
`DKON`.`Node` `n`
ORDER BY
`n`.`stakedBalance` DESC
)
SELECT
`ranking_list`.`id` AS `id`,
`ranking_list`.`nickname` AS `nickname`,
`ranking_list`.`address` AS `address`,
round((`ranking_list`.`stakedBalance` / pow(10, 18)), 4) AS `stakedBalance`,
round((`ranking_list`.`computingPower` / pow(10, 18)), 4) AS `computingPower`,
round(
(
`ranking_list`.`childrenAccumulatedBalance` / pow(10, 18)
),
4
) AS `childrenAccumulatedBalance`,
`ranking_list`.`stakedBalance` AS `stakedBalanceOrig`,
`ranking_list`.`computingPower` AS `computingPowerOrig`,
`ranking_list`.`childrenAccumulatedBalance` AS `childrenAccumulatedBalanceOrig`,
(
`ranking_list`.`stakedBalance` / (
SELECT
sum(`ranking_list`.`stakedBalance`)
FROM
`ranking_list`
)
) AS `staked_portion`,
(
`ranking_list`.`computingPower` / (
SELECT
sum(`ranking_list`.`computingPower`)
FROM
`ranking_list`
)
) AS `computingPower_portion`,
(
`ranking_list`.`childrenAccumulatedBalance` / (
SELECT
sum(`ranking_list`.`childrenAccumulatedBalance`)
FROM
`ranking_list`
)
) AS `childrenAccumulatedBalance_portion`,
(
(
`ranking_list`.`computingPower` / (
SELECT
sum(`ranking_list`.`computingPower`)
FROM
`ranking_list`
)
) * 66666.666666666666
) AS `computingPower_estimated_dist`
FROM
`ranking_list`
GROUP BY
`ranking_list`.`address`

50
src/admin/userlist.jsx Normal file
View File

@@ -0,0 +1,50 @@
import {
Box,
Pagination,
Table,
TableBody,
TableHead,
TableRow,
TableCell,
Text,
} from '@adminjs/design-system';
const axios = require('axios');
import { useEffect, useState } from 'react';
const React = require('react');
const userListDashboard = (props) => {
console.log('UserListDashboard', props);
const [data, setData] = useState([]);
useEffect(() => {
async function fetchData() {
const { data: res } = await axios.get(
'https://api.b-kon.io/v1/node/admin/rank-list',
);
setData(res);
}
fetchData();
}, []);
return (
<Box variant="container">
<Table>
<TableHead>ddd</TableHead>
<TableBody>
<TableRow>
<TableCell>aa</TableCell>
<TableCell>bb</TableCell>
<TableCell>cc {data}</TableCell>
</TableRow>
</TableBody>
</Table>
<Text mt="xl" textAlign="center">
<Pagination page={1} perPage={1000} total={10000} onChange={() => {}} />
</Text>
</Box>
);
};
export default userListDashboard;

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
// expect(appController.getHello()).toBe('Hello World!');
});
});
});

20
src/app.controller.ts Normal file
View File

@@ -0,0 +1,20 @@
import { Controller, Get, Post, Param } from '@nestjs/common';
import { AppService } from './app.service';
import { ApiCreatedResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
@Controller()
@ApiTags('기본 API')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('ping')
@ApiOperation({ summary: 'Hello World' })
getHello(): string {
return 'pong';
}
@Get('health')
getHealth(): string {
return this.appService.getHealth();
}
}

185
src/app.module.ts Normal file
View File

@@ -0,0 +1,185 @@
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './logging';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserController } from './user/user.controller';
import { SystemController } from './system/system.controller';
import { WalletController } from './wallet/wallet.controller';
import { NodeController } from './node/node.controller';
import { BoardController } from './board/board.controller';
import { UserService } from './user/user.service';
import { BoardService } from './board/board.service';
import { NodeService } from './node/node.service';
import { SystemService } from './system/system.service';
import { WalletService } from './wallet/wallet.service';
import { PrismaModule } from './prisma.module';
import { FirebaseService } from './firebase/firebase.service';
import { FirebaseController } from './firebase/firebase.controller';
import { JwtStrategy } from './middleware/jwt.strategy';
import { AuthService } from './auth/auth.service';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
import { PrismaClient } from '@prisma/client';
import { ScheduleModule } from '@nestjs/schedule';
import { Node2AddressModule } from './node2address/node2address.module';
import { LockedCoinModule } from './locked-coin/locked-coin.module';
import { TranferHistoriesModule } from './transferhistories/transferhistories.module';
const prisma = new PrismaClient();
const DEFAULT_ADMIN = {
email: 'code@mnco.dev',
password: 'MnCo0310!!@@',
};
const authenticate = async (email: string, password: string) => {
if (email === DEFAULT_ADMIN.email && password === DEFAULT_ADMIN.password) {
return Promise.resolve(DEFAULT_ADMIN);
}
return null;
};
@Module({
imports: [
import('@adminjs/nestjs').then(({ AdminModule }) => {
return import('adminjs').then(async ({ AdminJS, ComponentLoader }) => {
const componentLoader = new ComponentLoader();
const Components = {
MyInput: componentLoader.add(
'MyInput',
'./components/DecimalComponent',
),
// other custom components
};
console.log(Components.MyInput);
return import('@adminjs/prisma').then((AdminJSTypeORM) => {
AdminJS.registerAdapter({
Database: AdminJSTypeORM.Database,
Resource: AdminJSTypeORM.Resource,
});
return AdminModule.createAdminAsync({
useFactory: () => ({
adminJsOptions: {
rootPath: '/admin',
resources: [
{
resource: {
model: AdminJSTypeORM.getModelByName('Node'),
client: prisma,
},
options: {
properties: {
balance: {
components: {
// list: Components.MyInput,
// show: Components.MyInput,
},
},
stakedBalance: {
components: {
// list: Components.MyInput,
// show: Components.MyInput,
},
},
computingPower: {
components: {
// list: Components.MyInput,
// show: Components.MyInput,
},
},
},
},
},
{
resource: {
model: AdminJSTypeORM.getModelByName('User'),
client: prisma,
},
options: {},
},
{
resource: {
model: AdminJSTypeORM.getModelByName('Category'),
client: prisma,
},
options: {},
},
{
resource: {
model: AdminJSTypeORM.getModelByName('LockedCoin'),
client: prisma,
},
options: {},
},
],
componentLoader,
},
auth: {
authenticate,
cookieName: 'adminjs',
cookiePassword: 'secret!!',
},
sessionOptions: {
resave: true,
saveUninitialized: true,
secret: 'secret!!',
},
}),
});
});
});
}),
ScheduleModule.forRoot(),
PrismaModule,
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'static'),
exclude: ['/api*', '/admin*'],
}),
PassportModule.register({ defaultStrategy: 'jwt' }),
ConfigModule,
JwtModule.registerAsync({
useFactory: async () => ({
secretOrPrivateKey: 'MCNOWINS1010!!!!',
secret: 'MNCOWINS1010!!!!',
signOptions: {
expiresIn: `${600000}s`,
},
}),
}),
Node2AddressModule,
LockedCoinModule,
TranferHistoriesModule,
],
controllers: [
AppController,
UserController,
SystemController,
WalletController,
NodeController,
BoardController,
FirebaseController,
],
providers: [
AppService,
UserService,
BoardService,
NodeService,
SystemService,
WalletService,
FirebaseService,
AuthService,
JwtService,
JwtStrategy,
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}

25
src/app.service.ts Normal file
View File

@@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';
import * as fs from 'fs';
import * as path from 'path';
function readFile(p: string) {
try {
return fs.readFileSync(p, 'utf8').trim();
} catch {
return undefined;
}
}
const ROOT = process.cwd();
const shaPath = path.join(ROOT, 'commit-sha.txt');
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
getHealth(): string {
return `{'status' : 'ok', 'commit-sha' : ${readFile(shaPath)}}`;
}
}

919
src/assets/defi-abi.ts Normal file
View File

@@ -0,0 +1,919 @@
export default [
{
inputs: [
{
internalType: 'address',
name: '_bkon',
type: 'address',
},
],
stateMutability: 'nonpayable',
type: 'constructor',
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: 'address',
name: 'parent',
type: 'address',
},
{
indexed: true,
internalType: 'address',
name: 'child',
type: 'address',
},
],
name: 'Accepted',
type: 'event',
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: 'address',
name: 'node',
type: 'address',
},
],
name: 'Activated',
type: 'event',
},
{
inputs: [
{
internalType: 'address',
name: 'node',
type: 'address',
},
],
name: 'active',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: '_parent',
type: 'address',
},
{
internalType: 'address',
name: 'node',
type: 'address',
},
],
name: 'activeForce',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
internalType: 'address[]',
name: 'nodelist',
type: 'address[]',
},
],
name: 'actives',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
internalType: 'uint256',
name: 'amount',
type: 'uint256',
},
],
name: 'deposit',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
internalType: 'uint256',
name: 'round',
type: 'uint256',
},
{
internalType: 'uint256',
name: 'start',
type: 'uint256',
},
{
internalType: 'uint256',
name: 'end',
type: 'uint256',
},
],
name: 'distribute',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
anonymous: false,
inputs: [
{
indexed: false,
internalType: 'uint256',
name: 'round',
type: 'uint256',
},
{
indexed: true,
internalType: 'address',
name: 'to',
type: 'address',
},
{
indexed: false,
internalType: 'uint256',
name: 'amount',
type: 'uint256',
},
],
name: 'Distributed',
type: 'event',
},
{
inputs: [
{
internalType: 'bytes32',
name: 'role',
type: 'bytes32',
},
{
internalType: 'address',
name: 'account',
type: 'address',
},
],
name: 'grantRole',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
internalType: 'uint256',
name: 'round',
type: 'uint256',
},
{
internalType: 'address[]',
name: 'recipient',
type: 'address[]',
},
{
internalType: 'uint256[]',
name: 'amount',
type: 'uint256[]',
},
],
name: 'inject',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: 'address',
name: 'previousOwner',
type: 'address',
},
{
indexed: true,
internalType: 'address',
name: 'newOwner',
type: 'address',
},
],
name: 'OwnershipTransferred',
type: 'event',
},
{
inputs: [],
name: 'renounceOwnership',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
internalType: 'bytes32',
name: 'role',
type: 'bytes32',
},
{
internalType: 'address',
name: 'account',
type: 'address',
},
],
name: 'renounceRole',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: 'mother',
type: 'address',
},
],
name: 'request',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: 'address',
name: 'parent',
type: 'address',
},
{
indexed: true,
internalType: 'address',
name: 'child',
type: 'address',
},
],
name: 'Request',
type: 'event',
},
{
inputs: [
{
internalType: 'bytes32',
name: 'role',
type: 'bytes32',
},
{
internalType: 'address',
name: 'account',
type: 'address',
},
],
name: 'revokeRole',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: 'bytes32',
name: 'role',
type: 'bytes32',
},
{
indexed: true,
internalType: 'bytes32',
name: 'previousAdminRole',
type: 'bytes32',
},
{
indexed: true,
internalType: 'bytes32',
name: 'newAdminRole',
type: 'bytes32',
},
],
name: 'RoleAdminChanged',
type: 'event',
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: 'bytes32',
name: 'role',
type: 'bytes32',
},
{
indexed: true,
internalType: 'address',
name: 'account',
type: 'address',
},
{
indexed: true,
internalType: 'address',
name: 'sender',
type: 'address',
},
],
name: 'RoleGranted',
type: 'event',
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: 'bytes32',
name: 'role',
type: 'bytes32',
},
{
indexed: true,
internalType: 'address',
name: 'account',
type: 'address',
},
{
indexed: true,
internalType: 'address',
name: 'sender',
type: 'address',
},
],
name: 'RoleRevoked',
type: 'event',
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: 'address',
name: 'from',
type: 'address',
},
{
indexed: true,
internalType: 'address',
name: 'to',
type: 'address',
},
{
indexed: false,
internalType: 'uint256',
name: 'amount',
type: 'uint256',
},
],
name: 'Send',
type: 'event',
},
{
inputs: [
{
internalType: 'uint256',
name: '_activeFee',
type: 'uint256',
},
{
internalType: 'uint256',
name: '_transferFee',
type: 'uint256',
},
],
name: 'setFee',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: 'recipient',
type: 'address',
},
{
internalType: 'uint256',
name: 'amount',
type: 'uint256',
},
],
name: 'transfer',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: 'sender',
type: 'address',
},
{
internalType: 'address',
name: 'recipient',
type: 'address',
},
{
internalType: 'uint256',
name: 'amount',
type: 'uint256',
},
],
name: 'transferFrom',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: 'newOwner',
type: 'address',
},
],
name: 'transferOwnership',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
internalType: 'uint256',
name: 'round',
type: 'uint256',
},
{
internalType: 'uint256',
name: 'start',
type: 'uint256',
},
{
internalType: 'address[]',
name: 'recipient',
type: 'address[]',
},
{
internalType: 'uint256[]',
name: 'amount',
type: 'uint256[]',
},
],
name: 'update',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
internalType: 'uint256',
name: 'amount',
type: 'uint256',
},
],
name: 'withdraw',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [],
name: 'activeFee',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: '',
type: 'address',
},
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
name: 'activeRequest',
outputs: [
{
internalType: 'address',
name: '',
type: 'address',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: '',
type: 'address',
},
],
name: 'activeRequested',
outputs: [
{
internalType: 'bool',
name: '',
type: 'bool',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'ADMIN_ROLE',
outputs: [
{
internalType: 'bytes32',
name: '',
type: 'bytes32',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: '',
type: 'address',
},
],
name: 'balance',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: 'node',
type: 'address',
},
],
name: 'balanceOf',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: '',
type: 'address',
},
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
name: 'child',
outputs: [
{
internalType: 'address',
name: '',
type: 'address',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'DEFAULT_ADMIN_ROLE',
outputs: [
{
internalType: 'bytes32',
name: '',
type: 'bytes32',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'DISTRIBUTOR_ROLE',
outputs: [
{
internalType: 'bytes32',
name: '',
type: 'bytes32',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: '',
type: 'address',
},
],
name: 'enabled',
outputs: [
{
internalType: 'bool',
name: '',
type: 'bool',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'feeWallet',
outputs: [
{
internalType: 'address',
name: '',
type: 'address',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: 'node',
type: 'address',
},
],
name: 'getNodeInfo',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
{
internalType: 'bool',
name: '',
type: 'bool',
},
{
internalType: 'address',
name: '',
type: 'address',
},
{
internalType: 'address[]',
name: '',
type: 'address[]',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'bytes32',
name: 'role',
type: 'bytes32',
},
],
name: 'getRoleAdmin',
outputs: [
{
internalType: 'bytes32',
name: '',
type: 'bytes32',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'bytes32',
name: 'role',
type: 'bytes32',
},
{
internalType: 'address',
name: 'account',
type: 'address',
},
],
name: 'hasRole',
outputs: [
{
internalType: 'bool',
name: '',
type: 'bool',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
name: 'nodes',
outputs: [
{
internalType: 'address',
name: '',
type: 'address',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'owner',
outputs: [
{
internalType: 'address',
name: '',
type: 'address',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'address',
name: '',
type: 'address',
},
],
name: 'parent',
outputs: [
{
internalType: 'address',
name: '',
type: 'address',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'bytes4',
name: 'interfaceId',
type: 'bytes4',
},
],
name: 'supportsInterface',
outputs: [
{
internalType: 'bool',
name: '',
type: 'bool',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
name: 'targetAddress',
outputs: [
{
internalType: 'address',
name: '',
type: 'address',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
name: 'targetAmount',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
name: 'targetDropped',
outputs: [
{
internalType: 'bool',
name: '',
type: 'bool',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [],
name: 'transferFee',
outputs: [
{
internalType: 'uint256',
name: '',
type: 'uint256',
},
],
stateMutability: 'view',
type: 'function',
},
];

12
src/assets/erc20-abi.ts Normal file
View File

@@ -0,0 +1,12 @@
export default [
'function balanceOf(address _owner) view returns (uint256)',
'function owner() view returns (address)',
'function name() view returns (string)',
'function totalSupply() view returns (uint256)',
'function decimals() view returns (uint8)',
'function symbol() view returns (string)',
'function allowance(address owner, address spender) external view returns (uint256)',
'function transfer(address _to, uint256 value) returns (bool success)',
'event Transfer(address indexed from, address indexed to, uint amount)',
'event Approval(address indexed owner, address indexed spender, uint256 value)',
];

50
src/assets/i18n.ts Normal file
View File

@@ -0,0 +1,50 @@
export default {
'withdraw.title': {
ko: '',
en: '',
zh: '',
vi: '',
ja: '',
th: '',
},
'deposit.title': {
ko: '',
en: '',
zh: '',
vi: '',
ja: '',
th: '',
},
'active.title': {
ko: '',
en: '',
zh: '',
vi: '',
ja: '',
th: '',
},
'withdraw.message': {
ko: '',
en: '',
zh: '',
vi: '',
ja: '',
th: '',
},
'deposit.message': {
ko: '',
en: '',
zh: '',
vi: '',
ja: '',
th: '',
},
'active.message': {
ko: '',
en: '',
zh: '',
vi: '',
ja: '',
th: '',
},
} as { [key: string]: { [key: string]: string } };

View File

@@ -0,0 +1,18 @@
export default [
'function aggregate(tuple(address target, bytes callData)[] calls) view returns (uint256 blockNumber, bytes[] returnData)',
'function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) payable returns (tuple(bool success, bytes returnData)[] returnData)',
'function aggregate3Value(tuple(address target, bool allowFailure, uint256 value, bytes callData)[] calls) payable returns (tuple(bool success, bytes returnData)[] returnData)',
'function blockAndAggregate(tuple(address target, bytes callData)[] calls) payable returns (uint256 blockNumber, bytes32 blockHash, tuple(bool success, bytes returnData)[] returnData)',
'function getBasefee() view returns (uint256 basefee)',
'function getBlockHash(uint256 blockNumber) view returns (bytes32 blockHash)',
'function getBlockNumber() view returns (uint256 blockNumber)',
'function getChainId() view returns (uint256 chainid)',
'function getCurrentBlockCoinbase() view returns (address coinbase)',
'function getCurrentBlockDifficulty() view returns (uint256 difficulty)',
'function getCurrentBlockGasLimit() view returns (uint256 gaslimit)',
'function getCurrentBlockTimestamp() view returns (uint256 timestamp)',
'function getEthBalance(address addr) view returns (uint256 balance)',
'function getLastBlockHash() view returns (bytes32 blockHash)',
'function tryAggregate(bool requireSuccess, tuple(address target, bytes callData)[] calls) payable returns (tuple(bool success, bytes returnData)[] returnData)',
'function tryBlockAndAggregate(bool requireSuccess, tuple(address target, bytes callData)[] calls) payable returns (uint256 blockNumber, bytes32 blockHash, tuple(bool success, bytes returnData)[] returnData)',
];

View File

@@ -0,0 +1,26 @@
const abi = [
{
inputs: [
{
internalType: 'address',
name: 'contractAddr',
type: 'address',
},
{
internalType: 'address[]',
name: 'addrs',
type: 'address[]',
},
{
internalType: 'uint256[]',
name: 'amounts',
type: 'uint256[]',
},
],
name: 'batchTransfer',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
];
export default abi;

30
src/auth/auth.service.ts Normal file
View File

@@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { User } from '@prisma/client';
import { UserService } from '../user/user.service';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from '../prisma.service';
@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
private readonly prisma: PrismaService,
) {}
public async getToken(userId: string) {
var user = await this.prisma.user.findUnique({
where: {
userId: userId,
},
});
const payload = { userId: user.userId };
const token = this.jwtService.sign(payload, {
secret: 'MNCOWINS1010!!!!',
});
return token;
}
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BoardController } from './board.controller';
describe('BoardController', () => {
let controller: BoardController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [BoardController],
}).compile();
controller = module.get<BoardController>(BoardController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,132 @@
import { Controller, Get, Post, Param, Body, Res } from '@nestjs/common';
import {
ApiCreatedResponse,
ApiOperation,
ApiTags,
ApiBody,
ApiProperty,
} from '@nestjs/swagger';
import { Board as BoardModel } from '@prisma/client';
import { BoardService } from './board.service';
import dayjs from 'dayjs';
import { Cron, CronExpression } from '@nestjs/schedule';
@Controller({
path: 'board',
version: '1',
})
@ApiTags('CS용 API')
export class BoardController {
constructor(private readonly boardService: BoardService) {}
@Get('/makeExcel')
@ApiOperation({ summary: '엑셀파일 생성', description: '엑셀파일 생성' })
@ApiCreatedResponse({
description: 'The user has been successfully created.',
})
async getExcel(@Res() res, @Param('date') date: string): Promise<any> {
var mydate = date
? dayjs(dayjs(date).format('YYYY-MM-DD'))
: dayjs(dayjs().format('YYYY-MM-DD'));
res.sendFile(__dirname + `snapshot_${mydate.format('YYYY-MM-DD')}.csv`);
}
@Cron('0 0 0 * * *')
@Post('/makeExcel')
@ApiOperation({ summary: '엑셀파일 생성', description: '엑셀파일 생성' })
@ApiCreatedResponse({
description: 'The user has been successfully created.',
})
async makeExcel(@Body() body: any): Promise<any> {
const { date } = body;
var mydate = date
? dayjs(dayjs(date).format('YYYY-MM-DD'))
: dayjs(dayjs().format('YYYY-MM-DD'));
return await this.boardService.generateSnapshotExcel(mydate);
}
@Get('/toc')
@ApiOperation({
summary: '이용약관 조회',
description: '이용약관 Board 데이터 회수',
})
getToc(): Promise<BoardModel> {
return this.boardService.getToc();
}
@Get('/terms')
@ApiOperation({
summary: '전자금융거래약관 조회',
description: '전자금융거래약관 Board 데이터 회수',
})
getTerm(): Promise<BoardModel> {
return this.boardService.getTerm();
}
@Get('/privacy')
@ApiOperation({
summary: '개인정보처리방침 조회',
description: '개인정보처리방침 Board 데이터 회수',
})
getPrivacy(): Promise<BoardModel> {
return this.boardService.getPrivacy();
}
@Get('/announce')
@ApiOperation({
summary: '공지사항 리스트 조회',
description: '공지사항 Board 데이터 회수',
})
getAccounces(): Promise<BoardModel[]> {
return this.boardService.getNotices();
}
@Get('/announce/:id')
@ApiOperation({
summary: '공지사항 조회',
description: '공지사항 Board 데이터 회수',
})
getAccounce(@Param('id') id: number): Promise<BoardModel> {
return this.boardService.getNotice(id);
}
@Get('/faq')
@ApiOperation({
summary: 'FAQ 리스트 조회',
description: 'FAQ Board 데이터 회수',
})
getFaqs(): Promise<BoardModel[]> {
return this.boardService.getFaqs();
}
@Get('/faq/:id')
@ApiOperation({ summary: 'FAQ 조회', description: '' })
getFaq(@Param('id') id: number): Promise<BoardModel> {
return this.boardService.getFaq(id);
}
@Get('/banner')
@ApiOperation({ summary: '배너 리스트 가져오기', description: '' })
getBanners(): Promise<BoardModel[]> {
return this.boardService.getBanners();
}
@Get('/banner/id')
@ApiOperation({ summary: '배너 가져오기', description: '' })
getBanner(@Param('id') id: number): Promise<BoardModel> {
return this.boardService.getBanner(id);
}
@Get('/:key')
@ApiOperation({
summary: '시스템 정보 가져오기',
description: 'key에대한 시스템 정보를 가져온다',
})
async getSystemInfo(@Param('key') key: string): Promise<{ value: string }> {
const systemInfo = await this.boardService.getByTitle(key);
if (!systemInfo) {
throw new Error(`System info with key ${key} not found`);
}
return { value: systemInfo.body };
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { BoardService } from './board.service';
import { BoardController } from './board.controller';
@Module({
providers: [BoardService],
controllers: [BoardController],
})
export class BoardModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { BoardService } from './board.service';
describe('BoardService', () => {
let service: BoardService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [BoardService],
}).compile();
service = module.get<BoardService>(BoardService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

595
src/board/board.service.ts Normal file
View File

@@ -0,0 +1,595 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma.service';
import { Board } from '@prisma/client';
import {
findClosestBlockByTimestamp,
getDeFiContract,
getTokenContract,
} from 'src/jsonrpc';
import dayjs from 'dayjs';
const wait = require('waait');
let converter = require('json-2-csv');
const fs = require('fs');
const path = require('path');
import Decimal from 'decimal.js';
import { Cron, CronExpression } from '@nestjs/schedule';
@Injectable()
export class BoardService {
constructor(private prisma: PrismaService) {}
cache = new Map();
async generateSnapshotExcel(date: dayjs.Dayjs) {
const ethers = require('ethers');
const wait = require('waait');
const Decimal = require('decimal.js');
let converter = require('json-2-csv');
const provider = new ethers.JsonRpcProvider('https://api.kon-wallet.com');
const defiAddress = '0xb4e1b1c8796e7b5f2c607ea43d21ec68a0052bdc';
const erc20Address = '0x1aF885C7DF29de8A4678BC8b69e84C980C9ab856';
/*
id,
address,
balance,
stakedBalance,
computingPower,
parentId,
isActivated,
nickname,
referral,
createdAt,
updatedAt,
userId,
childrenAccumulatedBalance,
childrenAccumulatedCount,
childrenAccumulatedActiveCount,
childrenAccumulatedDeactiveCount,
childrenAccumulatedStakedBalance,
childrenDepth,
depth
*/
const defiAbi = [
'function balanceOf(address _owner) view returns (uint256)',
];
const erc20Abi = [
'function balanceOf(address _owner) view returns (uint256)',
'function owner() view returns (address)',
'function name() view returns (string)',
'function totalSupply() view returns (uint256)',
'function decimals() view returns (uint8)',
'function symbol() view returns (string)',
'function allowance(address owner, address spender) external view returns (uint256)',
'function transfer(address _to, uint256 value) returns (bool success)',
'event Transfer(address indexed from, address indexed to, uint amount)',
'event Approval(address indexed owner, address indexed spender, uint256 value)',
];
const targets = ['240426'];
const records: any[] = await this.prisma.node.findMany();
const defiContract = new ethers.Contract(defiAddress, defiAbi, provider);
const erc20Contract = new ethers.Contract(erc20Address, erc20Abi, provider);
const addresses = records.map((e) => e.address);
let list = [];
let fetchedBalanceList = [];
let nodes = {};
let node_addr_to_id = {};
let child_list = {};
async function fetch(blockTag) {
let cnt = 0;
const fetchAddressList = [];
for (const address of addresses) {
list.push(get(address, blockTag));
//console.log(list)
fetchAddressList.push(address);
if (cnt % 100 == 0 && cnt > 0) {
//console.log(cnt)
let returnList = [];
try {
returnList = await Promise.allSettled(list);
await wait(1000);
} catch (e) {
console.error(e);
await wait(1000);
returnList = await Promise.allSettled(list);
}
//console.log(fetchedBalanceList)
fetchedBalanceList = [...fetchedBalanceList, ...returnList];
list = [];
}
cnt++;
}
let returnList = [];
try {
returnList = await Promise.allSettled(list);
await wait(1000);
} catch (e) {
console.error(e);
//await wait(1000)
//returnList = await Promise.allSettled(list)
}
fetchedBalanceList = [...fetchedBalanceList, ...returnList];
//console.log(fetchedBalanceList)
console.log(
'equal size : ',
fetchAddressList.length,
fetchedBalanceList.length,
fetchAddressList.length == fetchedBalanceList.length,
);
for (let i = 0; i < fetchAddressList.length; i++) {
if (fetchedBalanceList[i]?.status == 'fulfilled') {
const n = nodes[node_addr_to_id[fetchAddressList[i]]];
n.stakedBalance = new Decimal(fetchedBalanceList[i].value.toString());
nodes[node_addr_to_id[fetchAddressList[i]]] = n;
//console.log("inject staked balance >>> ", n)
} else {
console.error(
'cananot fetched balance : ',
fetchAddressList[i],
fetchedBalanceList[i],
);
}
}
return fetchedBalanceList;
}
async function get(address, blockTag) {
return defiContract.balanceOf(address, { blockTag: blockTag });
}
async function get2(address, blockTag) {
return erc20Contract.balanceOf(address, { blockTag: blockTag });
}
async function updateSnapshotCPsAll(blockTag) {
console.log('>>>>> updateSnapshotCPsAll:::buildNodeStructure start');
for (const node of records) {
node.stakedBalance = new Decimal(0);
node.balance = new Decimal(0);
node.computingPower = new Decimal(0);
node.childrenAccumulatedBalance = new Decimal(0);
node.childrenAccumulatedStakedBalance = new Decimal(0);
node['childrenUserAccumulatedCount'] = new Decimal(0);
node['childrenUserAccumulatedBalance'] = new Decimal(0);
node['childrenUserAccumulatedStakedBalance'] = new Decimal(0);
nodes[node.id] = node;
node_addr_to_id[node.address] = node.id;
console.log('init >>> ', node.id);
//console.log("init >>> ", nodes[node.id])
}
await fetch(blockTag);
await wait(100);
//await fetch2(blockTag)
/*2
for (const r of result) {
if (r.status == 'fulfilled') {
console.log(r.value)
} else {
console.log(r.reason)
}
}
*/
for (const node of records) {
if (node.parentId) {
if (child_list[node.parentId]) {
const newList = child_list[node.parentId];
newList.push(node.id);
child_list[node.parentId] = newList;
} else {
child_list[node.parentId] = [node.id];
}
}
}
for (const node of records) {
//console.log(node.id, ">>>", child_list[node.parentId], ">>>", node.parentId)
}
/*
let nodeId = "1";
for (const child of child_list[nodeId]) {
}
for (const node of records) {
console.log(node.id, node.nickname)
let cnt = 1;
let parent = nodes[node.parentId]
while (parent) {
console.log("-".repeat(cnt), parent.id, parent.nickname)
parent = nodes[parent.parentId] || null;
cnt++;
}
}
*/
console.log('>>>>> updateSnapshotCPsAll:::buildNodeStructure done');
console.log('>>>>> updateSnapshotCPsAll:::updateSnapshotCPsV2 start');
for (const n of records) {
if (n.stakedBalance > 0) {
console.log(n.id, n.stakedBalance);
}
updateSnapshotCPsV2(n.id);
}
console.log('>>>>> updateSnapshotCPsAll:::updateSnapshotCPsV2 done');
/*
for (const node of records) {
console.log(node.id, node.nickname)
let cnt = 1;
let parent = nodes[node.parentId]
while (parent) {
console.log("-".repeat(cnt), parent.id, parent.nickname)
parent = nodes[parent.parentId] || null;
cnt++;
}
}
*/
}
function updateSnapshotCPsV2(id) {
const node = nodes[id];
if (!node) {
console.error('Node not found : ', id);
return;
}
//const node_ = node[address];
updateSnapshotCPV2(node.id);
if (node.parentId) {
//console.log("go to parent >>> ", node.parentId)
let parent = nodes[node.parentId];
console.log(node.id, '>>> go to parent >>> ', parent.id);
while (parent) {
updateSnapshotCPV2(parent.id);
if (parent.parentId) {
parent = nodes[parent.parentId];
} else {
console.log('dead end >> : ', parent.parentId);
parent = null;
}
console.log('go to parent of parent >>> ', parent);
}
}
}
async function updateSnapshotCPV2(id) {
console.log('update : ', id);
const node = nodes[id];
if (!node) {
console.error('Node not found : ', id);
return;
}
node.computingPower = new Decimal(0);
node.childrenAccumulatedStakedBalance = new Decimal(0);
node.childrenAccumulatedBalance = new Decimal(0);
node.childrenUserAccumulatedCount = new Decimal(0);
//node.childrenUserAccumulatedCount = new Decimal(0);
node.childrenUserAccumulatedStakedBalance = new Decimal(0);
const chlidren = child_list[id];
// 자식들의 자산 합산
if (chlidren && chlidren.length > 0) {
for (const childId of chlidren) {
const child = nodes[childId];
node.childrenAccumulatedStakedBalance =
node.childrenAccumulatedStakedBalance
.add(child.stakedBalance)
.add(child.childrenAccumulatedStakedBalance);
node.childrenAccumulatedBalance = node.childrenAccumulatedBalance
.add(child.balance)
.add(child.childrenAccumulatedBalance);
const cp = calcComputingPower(
child.childrenAccumulatedStakedBalance.add(child.stakedBalance),
);
node.computingPower = node.computingPower.add(cp);
if (child.userId && child.userId != node.userId) {
node.childrenUserAccumulatedBalance =
node.childrenUserAccumulatedBalance.add(child.balance);
node.childrenUserAccumulatedStakedBalance =
node.childrenUserAccumulatedStakedBalance.add(
child.stakedBalance,
);
}
if (
child.userId &&
child.stakedBalance.gte(200_000_000_000_000_000_000)
) {
node.childrenUserAccumulatedCount =
node.childrenUserAccumulatedCount.add(1);
}
node.childrenUserAccumulatedBalance =
node.childrenUserAccumulatedBalance.add(
child.childrenAccumulatedBalance,
);
node.childrenUserAccumulatedStakedBalance =
node.childrenUserAccumulatedStakedBalance.add(
child.childrenAccumulatedStakedBalance,
);
node.childrenUserAccumulatedCount =
node.childrenUserAccumulatedCount.add(
child.childrenUserAccumulatedCount,
);
}
// 자식 중 최대 컴퓨팅 파워의 경우 세제곱근 연산
let max = new Decimal(0);
for (const childId of chlidren) {
const c = nodes[childId];
const childrenTotal = c.childrenAccumulatedStakedBalance.add(
c.stakedBalance,
);
if (max.lt(childrenTotal)) {
max = childrenTotal;
}
}
node.computingPower = node.computingPower
.add(max.cbrt().mul(10 ** 12))
.sub(calcComputingPower(max));
}
nodes[id] = {
...nodes[id],
computingPower: node.computingPower,
childrenAccumulatedBalance: node.childrenAccumulatedBalance,
childrenAccumulatedStakedBalance: node.childrenAccumulatedStakedBalance,
};
//console.log(nodes[id])
}
function calcComputingPower(amount) {
// 자식의 파워 계산 (1만까지는 10배수, 그 이후는 1배수)
//console.log(amount.div(10**18).toFixed(2), (new Decimal(10_000)).toFixed(2), amount.gte(new Decimal(10_000).mul(10 ** 18)))
if (amount.gte(new Decimal(10_000).mul(10 ** 18))) {
return amount.add(new Decimal(10_000 * 9).mul(10 ** 18));
} else {
return amount.mul(10);
}
}
async function main() {
for (const target_date of targets) {
list = [];
fetchedBalanceList = [];
nodes = {};
node_addr_to_id = {};
child_list = {};
//const target_date = 231126
const blockTag = await findClosestBlockByTimestamp(
date.toDate().getTime() / 1000,
);
await updateSnapshotCPsAll(blockTag);
const lastList = [];
let total_staked_amount = new Decimal(0);
let total_cp_amount = new Decimal(0);
for (const address of addresses) {
const id = node_addr_to_id[address];
if (nodes[id].stakedBalance.gte(200_000_000_000_000_000_000)) {
const pruned = nodes[id];
total_staked_amount = total_staked_amount.add(pruned.stakedBalance);
total_cp_amount = total_cp_amount.add(pruned.computingPower);
}
}
for (const address of addresses) {
const id = node_addr_to_id[address];
if (nodes[id].stakedBalance.gte(200_000_000_000_000_000_000)) {
//if (nodes[id].stakedBalance.gt(0) || nodes[id].balance.gt(0)) {
const pruned = nodes[id];
delete pruned.balance;
pruned['stakedBalance_pruned'] = pruned.stakedBalance
.div(10 ** 18)
.toString();
pruned['computingPower_pruned'] = pruned.computingPower
.div(10 ** 18)
.toString();
pruned['childrenAccumulatedBalance_pruned'] =
pruned.childrenAccumulatedBalance.div(10 ** 18).toString();
pruned['childrenAccumulatedStakedBalance_pruned'] =
pruned.childrenAccumulatedStakedBalance.div(10 ** 18).toString();
pruned['childrenUserAccumulatedCount_pruned'] =
pruned.childrenUserAccumulatedCount.div(10 ** 18);
pruned['childrenUserAccumulatedStakedBalance_pruned'] =
pruned.childrenUserAccumulatedStakedBalance
.div(10 ** 18)
.toString();
pruned['childrenUserAccumulatedBalance_pruned'] =
pruned.childrenUserAccumulatedBalance.div(10 ** 18).toString();
pruned.childrenAccumulatedBalance =
pruned.childrenAccumulatedBalance.toString();
pruned.childrenAccumulatedStakedBalance =
pruned.childrenAccumulatedStakedBalance.toString();
pruned.childrenUserAccumulatedCount =
pruned.childrenUserAccumulatedCount.toString();
pruned.childrenUserAccumulatedStakedBalance =
pruned.childrenUserAccumulatedStakedBalance.toString();
pruned.childrenUserAccumulatedBalance =
pruned.childrenUserAccumulatedBalance.toString();
//pruned['ranking_estimated_dist'] = pruned.stakedBalance.mul(dist_amount).div(total_staked_amount).toString();
//pruned['computingPower_estimated_dist'] = pruned.computingPower.mul(dist_amount).div(total_cp_amount).toString();
pruned['ranking_estimated_dist'] = -1;
pruned['computingPower_estimated_dist'] = -1;
pruned['rank'] = -1;
pruned.stakedBalance = pruned.stakedBalance.toString();
//pruned.balance = pruned.balance.toString();
pruned.computingPower = pruned.computingPower.toString();
lastList.push(pruned);
}
}
const csvFileData = await converter.json2csv(lastList);
fs.writeFileSync(
__dirname + `snapshot_${date.format('YYYY-MM-DD')}.csv`,
csvFileData,
);
return __dirname + `snapshot_${date.format('YYYY-MM-DD')}.csv`;
}
}
main()
.then((res) => {
console.log('done', res);
})
.catch((e) => {
console.error(e);
});
}
async getToc(): Promise<Board | null> {
const category = 'toc';
if (!this.cache.has(category)) {
const res = await this.prisma.category.findFirst({
where: { name: category },
});
this.cache.set(category, res.id);
}
return this.prisma.board.findFirst({
where: { category: { id: this.cache.get(category) } },
});
}
async getTerm(): Promise<Board | null> {
const category = 'term';
if (!this.cache.has(category)) {
const res = await this.prisma.category.findFirst({
where: { name: category },
});
this.cache.set(category, res.id);
}
return this.prisma.board.findFirst({
where: { category: { id: this.cache.get(category) } },
});
}
async getPrivacy(): Promise<Board | null> {
const category = 'privacy';
if (!this.cache.has(category)) {
const res = await this.prisma.category.findFirst({
where: { name: category },
});
this.cache.set(category, res.id);
}
return this.prisma.board.findFirst({
where: { category: { id: this.cache.get(category) } },
});
}
async getNotices(): Promise<Board[] | null> {
const category = 'notice';
if (!this.cache.has(category)) {
const res = await this.prisma.category.findFirst({
where: { name: category },
});
this.cache.set(category, res.id);
}
return this.prisma.board.findMany({
where: { category: { id: this.cache.get(category) } },
});
}
async getNotice(id: number): Promise<Board | null> {
const category = 'notice';
if (!this.cache.has(category)) {
const res = await this.prisma.category.findFirst({
where: { name: category },
});
this.cache.set(category, res.id);
}
return this.prisma.board.findFirst({
where: { category: { id: this.cache.get(category) } },
});
}
async getFaqs(): Promise<Board[] | null> {
const category = 'faq';
if (!this.cache.has(category)) {
const res = await this.prisma.category.findFirst({
where: { name: category },
});
this.cache.set(category, res.id);
}
return this.prisma.board.findMany({
where: { category: { id: this.cache.get(category) } },
});
}
async getFaq(id: number): Promise<Board | null> {
const category = 'faq';
if (!this.cache.has(category)) {
const res = await this.prisma.category.findFirst({
where: { name: category },
});
this.cache.set(category, res.id);
}
return this.prisma.board.findFirst({
where: { category: { id: this.cache.get(category) } },
});
}
async getBanners(): Promise<Board[] | null> {
const category = 'banner';
if (!this.cache.has(category)) {
const res = await this.prisma.category.findFirst({
where: { name: category },
});
this.cache.set(category, res.id);
}
return this.prisma.board.findMany({
where: { category: { id: this.cache.get(category) } },
});
}
async getBanner(id: number): Promise<Board | null> {
const category = 'banner';
if (!this.cache.has(category)) {
const res = await this.prisma.category.findFirst({
where: { name: category },
});
this.cache.set(category, res.id);
}
return this.prisma.board.findFirst({
where: { category: { id: this.cache.get(category) } },
});
}
async getByTitle(title: string): Promise<Board | null> {
const board = await this.prisma.board.findFirst({
where: { title },
});
if (!board) {
throw new Error(`Board with title ${title} not found`);
}
return board;
}
}

View File

@@ -0,0 +1,15 @@
import React from 'react';
const DecimalComponent: React.FC = (props: any) => {
const { record, property } = props;
const originalValue = record?.params[property.path] ?? 0;
const transformedValue = (originalValue / 1e18).toFixed(4);
return (
<div>
<span>{transformedValue}</span>
</div>
);
};
export default DecimalComponent;

204
src/cron.ts Normal file
View File

@@ -0,0 +1,204 @@
import cron from 'node-cron';
import {
ContractEventName,
DeferredTopicFilter,
EventLog,
TopicFilter,
ethers,
} from 'ethers';
import { PrismaService } from './prisma.service';
export class cronjob {
private prisma = new PrismaService();
private jobs = [];
constructor() {
// this.jobs.push(cron.schedule('*/5 * * * * *', BKONTrnasferEventCron(this.prisma)));
}
}
export class JSONRPCCall {
private jsonrpc: string;
private provider;
private ERC20abi = [
'function balanceOf(address _owner) view returns (uint256)',
'function owner() view returns (address)',
'function name() view returns (string)',
'function totalSupply() view returns (uint256)',
'function decimals() view returns (uint8)',
'function symbol() view returns (string)',
'function transfer(address _to, uint256 value) returns (bool success)',
'event Transfer(address indexed from, address indexed to, uint amount)',
];
private DeFiabi = [
'event Transfer(address indexed from, address indexed to, uint256 amount)',
'event Distributed(uint256 round, address indexed to, uint256 amount)',
'event Request(address indexed parent, address indexed child)',
'event Accepted(address indexed parent, address indexed child)',
'event Activated(address indexed node)',
];
constructor(jsonrpc: string) {
this.jsonrpc = jsonrpc;
this.provider = new ethers.JsonRpcProvider(this.jsonrpc);
}
async getERC20TranscationList(
contractAddress: string,
start: number,
end?: number,
): Promise<(ethers.EventLog | ethers.Log)[]> {
const contract = new ethers.Contract(
contractAddress,
this.ERC20abi,
this.provider,
);
const filter = contract.filters.Transfer(null, null);
const txes = await contract.queryFilter(filter, start, end ?? 'latest');
return txes;
}
async getDefiDistributedTranscationList(
contractAddress: string,
start: number,
end?: number,
): Promise<(ethers.EventLog | ethers.Log)[]> {
const contract = new ethers.Contract(
contractAddress,
this.DeFiabi,
this.provider,
);
const filter_transfer = contract.filters.Transfer(null, null);
const filter_distributed = contract.filters.Distributed(null, null);
const filter_request = contract.filters.Request(null, null);
const filter_accepted = contract.filters.Accepted(null, null);
const filter_activated = contract.filters.Activated(null);
const txes = await contract.queryFilter(
filter_distributed,
start,
end ?? 'latest',
);
return txes;
}
async getDefiRequestTranscationList(
contractAddress: string,
start: number,
end?: number,
): Promise<(ethers.EventLog | ethers.Log)[]> {
const contract = new ethers.Contract(
contractAddress,
this.DeFiabi,
this.provider,
);
const filter_request = contract.filters.Request(null, null);
const txes = await contract.queryFilter(
filter_request,
start,
end ?? 'latest',
);
return txes;
}
async getDefiAcceptedTranscationList(
contractAddress: string,
start: number,
end?: number,
): Promise<(ethers.EventLog | ethers.Log)[]> {
const contract = new ethers.Contract(
contractAddress,
this.DeFiabi,
this.provider,
);
const filter_accepted = contract.filters.Accepted(null, null);
const txes = await contract.queryFilter(
filter_accepted,
start,
end ?? 'latest',
);
return txes;
}
async getDefiActivatedTranscationList(
contractAddress: string,
start: number,
end?: number,
): Promise<(ethers.EventLog | ethers.Log)[]> {
const contract = new ethers.Contract(
contractAddress,
this.DeFiabi,
this.provider,
);
const filter_accepted = contract.filters.Accepted(null, null);
const txes = await contract.queryFilter(
filter_accepted,
start,
end ?? 'latest',
);
return txes;
}
/*
async BKONTrnasferEventCron(prisma: PrismaService) {
const jsonrpc = 'https://api.kon-wallet.com';
const contractAddress = '0xFa4fcd169a04b94d47CdD9a2dB2637124fcD0Aa3';
const contract = new JSONRPCCall(jsonrpc);
const txes = await contract.getERC20TranscationList(contractAddress, 0);
for(const tx of txes) {
const _tx = await prisma.txes.findFirst({where:{txid: tx.transactionHash}});
const from = await prisma.node.findFirst({where:{address: (tx as EventLog).args.from}});
const to = await prisma.node.findFirst({where:{address: (tx as EventLog).args.to}});
if(_tx == null && (from != null || to != null)) {
await prisma.txes.create({
data: {
txid: tx.transactionHash,
from: (tx as EventLog).args.from,
to: (tx as EventLog).args.to,
type: "transfer",
value: (tx as EventLog).args.amount,
blockNumber: tx.blockNumber,
blockTimestamp: new Date(tx.blockNumber),
}
})
}
}
}
async DeFiTrnasferEventCron(prisma: PrismaService) {
const jsonrpc = 'https://api.kon-wallet.com';
const contractAddress = '0x6b123269F7c048B90d788Bb160ff9554Bf661496';
const contract = new JSONRPCCall(jsonrpc);
const txes = await contract.getDefiRequestTranscationList(contractAddress, 0);
const txes = await contract.getDefiRequestTranscationList(contractAddress, 0);
const txes = await contract.getDefiAcceptedTranscationList(contractAddress, 0);
const txes = await contract.getDefiRequestTranscationList(contractAddress, 0);
for(const tx of txes) {
const _tx = await prisma.txes.findFirst({where:{txid: tx.transactionHash}});
const from = await prisma.node.findFirst({where:{address: (tx as EventLog).args.from}});
const to = await prisma.node.findFirst({where:{address: (tx as EventLog).args.to}});
if(_tx == null && (from != null || to != null)) {
await prisma.txes.create({
data: {
txid: tx.transactionHash,
from: (tx as EventLog).args.from,
to: (tx as EventLog).args.to,
type: "transfer",
value: (tx as EventLog).args.amount,
blockNumber: tx.blockNumber,
blockTimestamp: new Date(tx.blockNumber),
}
})
}
}
}
*/
}

View File

@@ -0,0 +1,48 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsEmpty,
IsNotEmpty,
IsString,
IsJSON,
IsOptional,
IsObject,
} from 'class-validator';
export class PushReqDto {
@IsString()
@IsNotEmpty()
@ApiProperty({
example: 'title',
})
title: string;
@IsString()
@IsNotEmpty()
@ApiProperty({
example: 'message',
})
message: string;
@IsOptional()
@IsObject()
@ApiProperty({
example: {
test: 'test',
},
})
data: object;
@IsString()
@IsOptional()
@ApiProperty({
example: 'naver.com',
})
link: string;
@IsString()
@IsNotEmpty()
@ApiProperty({
enum: ['SYSTEM', 'USER', 'TRANSACTION'],
})
type: string;
}

View File

@@ -0,0 +1,115 @@
import { Test, TestingModule } from '@nestjs/testing';
import { FirebaseController } from './firebase.controller';
import { FirebaseService } from './firebase.service';
import { PrismaService } from '../prisma.service';
import { UserService } from '../user/user.service';
describe('FirebaseController', () => {
let controller: FirebaseController;
let prisma: jest.Mocked<PrismaService>;
let firebase: jest.Mocked<FirebaseService>;
beforeEach(async () => {
const prismaMock = {
// @ts-expect-error partial mock
user: {
findMany: jest.fn(),
},
// @ts-expect-error partial mock
userNotification: {
createMany: jest.fn(),
},
} as unknown as jest.Mocked<PrismaService>;
const firebaseMock = {
// @ts-expect-error partial mock
sendPushMessage: jest.fn(),
} as unknown as jest.Mocked<FirebaseService>;
const module: TestingModule = await Test.createTestingModule({
controllers: [FirebaseController],
providers: [
{ provide: FirebaseService, useValue: firebaseMock },
{ provide: PrismaService, useValue: prismaMock },
{ provide: UserService, useValue: {} },
],
}).compile();
controller = module.get<FirebaseController>(FirebaseController);
prisma = module.get(PrismaService) as any;
firebase = module.get(FirebaseService) as any;
});
it('pushNoti sends notifications to user tokens and stores logs', async () => {
const userId = 'user-123';
const users = [
{ userId, fcmToken: 'token-1' },
{ userId, fcmToken: null },
{ userId, fcmToken: 'token-2' },
] as any[];
(prisma.user.findMany as any).mockResolvedValue(users);
const body = {
title: 'Hello',
message: 'World',
data: { foo: 'bar' },
link: 'https://example.com',
type: 'SYSTEM',
} as any;
const sendResult = [['msg-1', 'msg-2']];
(firebase.sendPushMessage as any).mockResolvedValue(sendResult);
const result = await controller.pushNoti(body, userId);
expect(prisma.user.findMany).toHaveBeenCalledWith({ where: { userId } });
expect(firebase.sendPushMessage).toHaveBeenCalledWith({
tokens: ['token-1', 'token-2'],
title: body.title,
message: body.message,
data: body.data,
link: body.link,
});
expect(prisma.userNotification.createMany).toHaveBeenCalledWith({
data: users.map((user: any) => ({
userId: user.userId,
title: body.title,
body: body.message,
data: JSON.stringify(body.data),
link: body.link,
type: body.type as any,
})),
});
expect(result).toBe(sendResult);
});
it('pushNoti still logs when no tokens are present', async () => {
const userId = 'user-abc';
const users = [{ userId, fcmToken: null }] as any[];
(prisma.user.findMany as any).mockResolvedValue(users);
const body = {
title: 'No Tokens',
message: 'Test',
data: {},
link: undefined,
type: 'SYSTEM',
} as any;
(firebase.sendPushMessage as any).mockResolvedValue(undefined);
const result = await controller.pushNoti(body, userId);
expect(firebase.sendPushMessage).toHaveBeenCalledWith({
tokens: [],
title: body.title,
message: body.message,
data: body.data,
link: body.link,
});
expect(prisma.userNotification.createMany).toHaveBeenCalled();
expect(result).toBeUndefined();
});
});

View File

@@ -0,0 +1,91 @@
import { Body, Controller, Param, Post } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { FirebaseService } from './firebase.service';
import { PushReqDto } from './dto/push.req.dto';
import { ApiProperty, ApiTags } from '@nestjs/swagger';
import { PrismaService } from '../prisma.service';
@Controller('firebase')
@ApiTags('firebase')
export class FirebaseController {
constructor(
readonly userService: UserService,
readonly firebaseService: FirebaseService,
readonly prisma: PrismaService,
) {}
@ApiProperty()
@Post('push')
async pushNotiAll(@Body() body: PushReqDto) {
const users = await this.prisma.user.findMany({});
const tokens = users
.map((user) => {
return user.fcmToken;
})
.filter((token) => !!token);
return await this.firebaseService
.sendPushMessage({
tokens,
title: body.title,
message: body.message,
data: body.data,
link: body.link,
})
.then(async (res) => {
return await this.prisma.userNotification.createMany({
data: users.map((user) => {
return {
userId: user.userId,
title: body.title,
body: body.message,
data: JSON.stringify(body.data),
link: body.link,
type: body.type as any,
};
}),
});
});
}
@ApiProperty()
@Post('push/:userId')
async pushNoti(@Body() body: PushReqDto, @Param('userId') userId: string) {
const users = await this.prisma.user.findMany({
where: {
userId,
},
});
console.log(users);
const tokens = users
.map((user) => {
return user.fcmToken;
})
.filter((token) => !!token);
return await this.firebaseService
.sendPushMessage({
tokens,
title: body.title,
message: body.message,
data: body.data,
link: body.link,
})
.then(async (res) => {
await this.prisma.userNotification.createMany({
data: users.map((user) => {
return {
userId: user.userId,
title: body.title,
body: body.message,
data: JSON.stringify(body.data),
link: body.link,
type: body.type as any,
};
}),
});
return res;
});
}
}

View File

@@ -0,0 +1,4 @@
import { Module } from '@nestjs/common';
@Module({})
export class FirebaseModule {}

View File

@@ -0,0 +1,108 @@
import { FirebaseService } from './firebase.service';
import * as admin from 'firebase-admin';
// Mock firebase-admin messaging API (Jest hoists this above imports at runtime)
jest.mock('firebase-admin', () => ({
__esModule: true,
messaging: jest.fn(),
}));
describe('FirebaseService.sendPushMessage', () => {
let service: FirebaseService;
let sendMock: jest.Mock;
beforeEach(() => {
service = new FirebaseService();
sendMock = jest
.fn()
.mockImplementation(({ token }: { token: string }) =>
Promise.resolve(`mock:${token}`),
);
// Each call to admin.messaging() should return an object with our send mock
(admin.messaging as unknown as jest.Mock).mockReturnValue({
send: sendMock,
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('returns undefined and does not call messaging when tokens are empty', async () => {
const result = await service.sendPushMessage({
tokens: [],
data: {},
message: 'hello',
title: 'title',
});
expect(result).toBeUndefined();
expect(admin.messaging).not.toHaveBeenCalled();
expect(sendMock).not.toHaveBeenCalled();
});
it('sends a notification for each token and returns results', async () => {
const tokens = ['tokA', 'tokB', 'tokC'];
const result = await service.sendPushMessage({
tokens,
data: { foo: 'bar' },
message: 'Body text',
title: 'Title text',
link: 'https://example.com',
});
// Single batch result with all message IDs
expect(Array.isArray(result)).toBe(true);
expect(result!.length).toBe(1);
expect(result![0]).toEqual(tokens.map((t) => `mock:${t}`));
// Called once per token
expect(admin.messaging).toHaveBeenCalledTimes(tokens.length);
expect(sendMock).toHaveBeenCalledTimes(tokens.length);
// Verify payload shape for first token
expect(sendMock).toHaveBeenCalledWith(
expect.objectContaining({
token: 'tokA',
notification: {
title: 'Title text',
body: 'Body text',
},
data: expect.objectContaining({
foo: 'bar',
webpush: 'https://example.com',
title: 'Title text',
body: 'Body text',
}),
android: expect.objectContaining({ priority: 'high' }),
webpush: { fcmOptions: { link: 'https://example.com' } },
}),
);
});
it('splits tokens into batches of 1000 and preserves order', async () => {
const tokens = Array.from({ length: 1001 }, (_, i) => `t${i}`);
const result = await service.sendPushMessage({
tokens,
data: {},
message: 'M',
title: 'T',
});
expect(result).toHaveLength(2);
expect(result![0]).toHaveLength(1000);
expect(result![1]).toHaveLength(1);
// Flatten and compare order/content
const flat = result!.flat();
expect(flat).toEqual(tokens.map((t) => `mock:${t}`));
// Messaging called once per token
expect(admin.messaging).toHaveBeenCalledTimes(tokens.length);
expect(sendMock).toHaveBeenCalledTimes(tokens.length);
});
});

View File

@@ -0,0 +1,62 @@
import { Injectable } from '@nestjs/common';
import * as admin from 'firebase-admin';
@Injectable()
export class FirebaseService {
sendPush(tokens: string[], title: string, body: any) {}
async sendPushMessage(pram: {
tokens: string[];
data: any;
message: string;
title: string;
link?: string;
}): Promise<any> {
if (pram.tokens.length == 0) {
return;
}
const tokenSlice: string[][] = [];
const r = Math.ceil(pram.tokens.length / 1000);
for (let index = 0; index < r; index++) {
const endIndex =
index * 1000 + 1000 > pram.tokens.length - 1
? pram.tokens.length
: index * 1000 + 1000;
tokenSlice.push(pram.tokens.slice(index * 1000, endIndex));
}
const notices = [] as string[][];
for (let index = 0; index < tokenSlice.length; index++) {
const element = tokenSlice[index];
const results = await Promise.all(
element.map((token) =>
admin.messaging().send({
token,
notification: {
title: pram.title,
body: pram.message,
},
data: {
...pram.data,
webpush: pram.link ?? '',
title: pram.title,
body: pram.message,
},
android: {
priority: 'high',
collapseKey: `${pram.title}`,
},
...(pram.link
? { webpush: { fcmOptions: { link: pram.link } } }
: {}),
}),
),
);
notices.push(results);
}
return notices;
}
}

250
src/index.js Normal file

File diff suppressed because one or more lines are too long

138
src/jsonrpc/index.ts Normal file
View File

@@ -0,0 +1,138 @@
import { ethers } from 'ethers';
import ERC20Abi from '../assets/erc20-abi';
import DeFiAbi from '../assets/defi-abi';
import ERC20MultisenderAbi from '../assets/multisender-abi';
let wallet: ethers.Wallet | undefined;
let defiContract: ethers.Contract | undefined;
let multiSenderContract: ethers.Contract | undefined;
let defiAddress: string | undefined;
let erc20Address: string | undefined;
let multisenderAddress: string | undefined;
let privateKey: string | undefined;
let initialized = false;
import { PrismaClient } from '@prisma/client';
export const provider = new ethers.JsonRpcProvider(
'https://api.kon-wallet.com',
);
const prisma = new PrismaClient();
export async function initEthers() {
const { value: _defiAddress } =
(await prisma.system.findUnique({ where: { key: 'defi-address' } })) || {};
const { value: _erc20Address } =
(await prisma.system.findUnique({ where: { key: 'erc20-address' } })) || {};
const { value: _multisenderAddress } = (await prisma.system.findUnique({
where: { key: 'multisender-address' },
}))!;
const { value: pk } =
(await prisma.system.findUnique({ where: { key: 'admin-private-key' } })) ||
{};
privateKey = pk;
defiAddress = _defiAddress;
erc20Address = _erc20Address;
multisenderAddress = _multisenderAddress;
initialized = true;
}
// 이더리움 RPC 프로바이더 설정
// 주어진 유닉스 타임스탬프와 가장 가까운 블록 높이를 찾는 함수
export async function findClosestBlockByTimestamp(targetTimestamp) {
let latestBlock = await provider.getBlock('latest');
let latestBlockNumber = latestBlock.number;
let earliestBlockNumber = 0;
// 이진 탐색을 사용하여 가장 가까운 블록 찾기
while (earliestBlockNumber <= latestBlockNumber) {
let middleBlockNumber = Math.floor(
(earliestBlockNumber + latestBlockNumber) / 2,
);
let middleBlock = await provider.getBlock(middleBlockNumber);
if (middleBlock.timestamp === targetTimestamp) {
return middleBlock.number;
} else if (middleBlock.timestamp < targetTimestamp) {
earliestBlockNumber = middleBlockNumber + 1;
} else {
latestBlockNumber = middleBlockNumber - 1;
}
}
// 가장 가까운 블록을 찾기 위한 추가 로직
let closestBlockNumber: number;
let closestBlockTimestampDifference = Infinity;
for (let blockNumber of [earliestBlockNumber, latestBlockNumber]) {
if (blockNumber >= 0 && blockNumber <= latestBlock.number) {
let block = await provider.getBlock(blockNumber);
let timestampDifference = Math.abs(block.timestamp - targetTimestamp);
if (timestampDifference < closestBlockTimestampDifference) {
closestBlockTimestampDifference = timestampDifference;
closestBlockNumber = block.number;
}
}
}
return closestBlockNumber;
}
export async function getWallet() {
if (!initialized) await initEthers();
if (!wallet) wallet = new ethers.Wallet(privateKey!, provider);
return wallet;
}
export async function getDeFiContract(): Promise<any> {
if (!initialized) await initEthers();
if (!wallet) wallet = new ethers.Wallet(privateKey!, provider);
if (!defiContract)
defiContract = new ethers.Contract(defiAddress!, DeFiAbi, wallet);
return defiContract;
}
export async function getTokenContract(): Promise<any> {
if (!initialized) await initEthers();
console.log('pk', privateKey);
if (!wallet) wallet = new ethers.Wallet(privateKey!, provider);
console.log('wallet:', wallet);
if (!defiContract)
defiContract = new ethers.Contract(erc20Address, ERC20Abi, wallet);
return defiContract;
}
export async function updateActive(nodeAddresses: string[]): Promise<string[]> {
const defiContract = getDeFiContract();
const nodes = await prisma.node.findMany({
where: {
address: {
in: nodeAddresses,
},
},
});
console.log(nodes.length);
var rtn = [];
for (const node of nodes) {
try {
console.log(node.address);
if (!node.isActivated) {
const enabled = await (await defiContract).enabled(node.address);
console.log(node.address, enabled);
if (enabled) {
var s = await prisma.node.update({
where: { address: node.address },
data: { isActivated: true },
});
rtn.push(node.address);
}
}
} catch (e) {
console.log(e);
}
}
return rtn;
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LockedCoinController } from './locked-coin.controller';
describe('LockedCoinController', () => {
let controller: LockedCoinController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [LockedCoinController],
}).compile();
controller = module.get<LockedCoinController>(LockedCoinController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,17 @@
import { Controller, Get, Param, Query } from '@nestjs/common';
import { LockedCoinService } from './locked-coin.service';
import { ApiProperty, ApiTags } from '@nestjs/swagger';
@ApiTags('locked-coin')
@Controller('locked-coin')
export class LockedCoinController {
constructor(private readonly lockedCoinService: LockedCoinService) {}
@Get()
@ApiProperty()
async byAddress(
@Query('address') address: string,
@Query('reason') reason: string,
) {
return await this.lockedCoinService.queryLockedCoin(address, reason);
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { LockedCoinService } from './locked-coin.service';
import { LockedCoinController } from './locked-coin.controller';
@Module({
providers: [LockedCoinService],
controllers: [LockedCoinController],
})
export class LockedCoinModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LockedCoinService } from './locked-coin.service';
describe('LockedCoinService', () => {
let service: LockedCoinService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [LockedCoinService],
}).compile();
service = module.get<LockedCoinService>(LockedCoinService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,49 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma.service';
@Injectable()
export class LockedCoinService {
constructor(private readonly prisma: PrismaService) {}
async queryLockedCoin(address: string, reason: string) {
const [lockedCoin, aggregateResult, notSended] =
await this.prisma.$transaction([
this.prisma.lockedCoin.findMany({
where: {
address: address,
reason: reason,
},
}),
this.prisma.lockedCoin.aggregate({
_sum: {
amount: true,
},
where: {
address: address,
reason: reason,
},
}),
this.prisma.lockedCoin.aggregate({
where: {
address: address,
reason: reason,
NOT: [
{
sendedAt: null,
},
],
},
_sum: {
amount: true,
},
}),
]);
return {
lockedCoin,
totalLocked: aggregateResult._sum.amount,
reamained: notSended ? notSended._sum.amount : 0,
};
}
}

132
src/logging.ts Normal file
View File

@@ -0,0 +1,132 @@
/**
* @fileoverview 요청/응답 로깅을 위한 인터셉터
* @module logging.interceptor
*/
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';
import { Request, Response } from 'express';
/**
* 요청과 응답을 로깅하는 인터셉터
* @class LoggingInterceptor
* @implements {NestInterceptor}
* @description
* - 모든 HTTP 요청과 응답을 데이터베이스에 로깅
* - 특정 경로는 로깅에서 제외
* - 민감한 정보(비밀번호 등)는 마스킹 처리
*/
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
/** 로깅에서 제외할 경로 목록 */
private logExcludePaths: string[] = [];
/** 로깅에서 제외할 시작 경로 목록 */
private logExcludePathsStartingWith: string[] = [];
/**
* LoggingInterceptor 생성자
* @constructor
* @param {Repository<Log>} logRepository - 로그 저장소
*/
constructor() {
this.logExcludePaths =
process.env.LOG_EXCLUDE_PATHS?.split(',')?.map((v) => v.trim()) || [];
this.logExcludePathsStartingWith =
process.env.LOG_EXCLUDE_PATHS_STARTING_WITH?.split(',')?.map((v) =>
v.trim(),
) || [];
}
/**
* 요청을 가로채서 로깅하는 메서드
* @method intercept
* @param {ExecutionContext} context - 실행 컨텍스트
* @param {CallHandler<any>} next - 다음 핸들러
* @returns {Promise<Observable<any>>} 응답 옵저버블
* @description
* - 요청 정보(IP, URL, 메서드, 바디 등) 로깅
* - 응답 데이터 로깅
* - 에러 발생 시 에러 정보 로깅
*/
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> {
const request: Request = context.switchToHttp().getRequest();
// get ip address
const ip = request.socket.remoteAddress;
const url = request.url;
const inExcludePaths = this.logExcludePaths.includes(url.split('?')[0]);
const inExcludePathsStartingWith = this.logExcludePathsStartingWith.some(
(path) => url.startsWith(path),
);
if (inExcludePaths || inExcludePathsStartingWith) {
console.log(
'\x1b[93m',
new Date().toLocaleString('ko-KR', { hour12: false }),
'EXC',
request.method.toString().toUpperCase(),
'\x1b[0m',
request.url,
'',
ip,
);
return next.handle();
}
console.log(
'\x1b[94m',
new Date().toLocaleString('ko-KR', { hour12: false }),
'REQ',
request.method.toString().toUpperCase(),
'\x1b[0m',
request.url,
JSON.stringify(request.body),
ip,
);
const startTime = Date.now();
return next.handle().pipe(
tap({
next: (response) => {
// exclude logs for admin/log in any case to prevent infinite loop
if (url.includes('admin/log')) {
return;
}
console.log(
'\x1b[93m',
new Date().toLocaleString('ko-KR', { hour12: false }),
'RES',
request.method.toString().toUpperCase(),
'\x1b[0m',
request.url,
JSON.stringify(response),
ip,
);
return response;
},
error: (error) => {
console.log(
'\x1b[91m',
new Date().toLocaleString('ko-KR', { hour12: false }),
'ERR',
request.method.toString().toUpperCase(),
'\x1b[0m',
request.url,
JSON.stringify(error),
ip,
);
return error;
},
}),
);
}
}

71
src/main.ts Normal file
View File

@@ -0,0 +1,71 @@
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
import {
ValidationPipe,
VersioningOptions,
VersioningType,
} from '@nestjs/common';
import * as admin from 'firebase-admin';
import { initializeApp } from 'firebase-admin/app';
import { PassportModule } from '@nestjs/passport';
declare global {
interface BigInt {
toJSON(): string;
}
}
BigInt.prototype.toJSON = function () {
return this.toString();
};
async function bootstrap() {
BigInt.prototype['toJSON'] = function () {
const int = Number.parseInt(this.toString());
return int ?? this.toString();
};
const params = {
projectId: process.env.project_id,
privateKeyId: process.env.private_key_id,
// privateKey: process.env.private_key.replace(/\\n/g, '\n'),
clientEmail: process.env.client_email,
clientId: process.env.client_id,
authUri: process.env.auth_uri,
tokenUri: process.env.token_uri,
authProviderX509CertUrl: process.env.auth_provider_x509_cert_url,
clientC509CertUrl: process.env.client_x509_cert_url,
};
// await initializeApp({
// credential: admin.credential.cert(params),
// databaseURL: process.env.firebase_url,
// });
const app = await NestFactory.create(AppModule);
app.enableCors();
app.useGlobalPipes(
new ValidationPipe({
transform: true,
transformOptions: { enableImplicitConversion: true },
}),
);
app.enableVersioning({
type: VersioningType.URI,
});
const config = new DocumentBuilder()
.setTitle('BKON API')
.setDescription('BKON API Service')
.setVersion('1.0.0')
.addTag('swagger')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('swagger', app, document);
await app.listen(process.env.PORT || 4000);
}
bootstrap();

View File

@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

View File

@@ -0,0 +1,19 @@
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserService } from '../user/user.service';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(readonly userService: UserService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: 'MNCOWINS1010!!!!',
});
}
async validate(payload: any) {
return await this.userService.findUserById(payload.userId);
}
}

View File

@@ -0,0 +1,55 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type, plainToClass } from 'class-transformer';
import {
IsBoolean,
IsEmpty,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
} from 'class-validator';
const optionalBooleanMapper = {
true: true,
false: false,
};
export default class NodeQueryDto {
@IsOptional()
@IsString()
@ApiProperty({
nullable: true,
required: false,
})
readonly status?: any;
@IsOptional()
@IsNumber()
@ApiProperty({
nullable: true,
required: false,
})
readonly depth?: number;
@IsOptional()
@IsString()
@ApiProperty({
nullable: true,
required: false,
})
readonly address?: string;
@IsOptional()
@IsString()
@ApiProperty({
nullable: true,
required: false,
})
readonly myAddress?: string;
@IsOptional()
@IsString()
@ApiProperty({
nullable: true,
required: false,
})
readonly nickname?: string;
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { NodeController } from './node.controller';
describe('NodeController', () => {
let controller: NodeController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [NodeController],
}).compile();
controller = module.get<NodeController>(NodeController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

289
src/node/node.controller.ts Normal file
View File

@@ -0,0 +1,289 @@
import {
Controller,
Get,
Post,
Param,
Body,
Query,
UseGuards,
Request,
Patch,
Delete,
Put,
} from '@nestjs/common';
import {
ApiCreatedResponse,
ApiOperation,
ApiTags,
ApiBody,
ApiProperty,
ApiBearerAuth,
ApiQuery,
} from '@nestjs/swagger';
import { Node as NodeModel } from '@prisma/client';
import { NodeService } from './node.service';
import { JwtAuthGuard } from '../middleware/jwt.auth.guard';
import NodeQueryDto from './dto/node.query.dto';
import { PrismaService } from '../prisma.service';
export class setReferralDto {
@ApiProperty({
example: '0xaaabbbccc',
description: 'address',
required: true,
})
address: string;
@ApiProperty({
example: 'refferal data',
description: 'refferal',
required: true,
})
referral: string;
@ApiProperty({
example: 'eth signed checksum',
description: 'checksum',
required: true,
})
checksum: string;
}
export class patchNodeDto {
@ApiProperty({
example: 'refferal data',
description: 'nick',
required: true,
})
nickname: string;
}
export class applyNodeRefferalDto {
@ApiProperty({
example: 'refferal data',
description: 'refferal',
required: true,
})
referralCode: string;
}
export class createNodeDto {
@ApiProperty({
example: '0xaaabbbccc',
description: 'address',
required: true,
})
address: string;
}
export class setNicknameDto {
@ApiProperty({
example: '0xaaabbbccc',
description: 'address',
required: true,
})
address: string;
@ApiProperty({
example: 'nickname data',
description: 'nickname',
required: true,
})
nickname: string;
@ApiProperty({
example: 'eth signed checksum',
description: 'checksum',
required: true,
})
checksum: string;
}
@Controller({
path: 'node',
version: '1',
})
@ApiTags('노드 채굴 API')
export class NodeController {
constructor(
private readonly nodeService: NodeService,
private readonly prisma: PrismaService,
) {}
@ApiQuery({
name: 'nickname',
example: 'nickname',
required: false,
})
@ApiQuery({
name: 'referralCode',
example: 'referralCode',
required: false,
})
@Get('/exist')
@ApiOperation({ summary: '', description: '' })
async checkNodeDuplicate(
@Query() query: { nickname?: string; referralCode?: string },
): Promise<any> {
const nodes = await this.nodeService.getNodeList(query);
return {
hasDuplicate: nodes.length > 0,
};
}
@Put('/need-check')
@ApiOperation({ summary: '', description: '' })
async getNeedCheckNode(@Body() body): Promise<any> {
const address = body.address;
return await this.nodeService.getNeedCheckNode(address);
}
@ApiProperty({})
@Patch('/rmLowerCase')
async getReferralList(): Promise<any> {
await this.prisma.node.findMany().then(async (nodes) => {
for (let node of nodes) {
await this.prisma.node.update({
where: { id: node.id },
data: {
referral: node.referral.toUpperCase(),
},
});
}
});
}
@Get('/referral/:code')
async getReferral(@Param('code') code: string): Promise<any> {
return await this.nodeService.findByReferral(code);
}
@Get('/:address')
getNodeByAddress(@Param('address') address: string): any {
return this.nodeService.getNodeByAddress(address);
}
@Post('/referral')
setReferral(@Body() body: setReferralDto): any {
return this.nodeService.setReferral(
body.address,
body.referral,
body.checksum,
);
}
@Get('/computing-power/:address')
getCP(@Param('address') address: string): any {
return this.nodeService.getNodeComputingPower(address);
}
@Get('/rank/:address')
getRank(@Param('address') address: string): any {
return this.nodeService.getNodeRank(address);
}
@Get('/admin/rank-list')
getAdminRankList(): any {
return this.nodeService.getNodeLevelList();
}
@Get('/active-node/:address')
getDeactiveNode(@Param('address') address: string): any {
return this.nodeService.getNodeActiveCount(address);
}
@Get('/deactive-node/:address')
@ApiOperation({ summary: '해당 유저의 Node 정보 Fetch', description: '' })
getActiveNode(@Param('address') address: string): any {
return this.nodeService.getNodeDeactiveCount(address);
}
@Get('/children-balance/:address')
@ApiOperation({ summary: '해당 유저의 Node 정보 Fetch', description: '' })
getChildrenBalance(@Param('address') address: string): any {
return this.nodeService.getChildrenStakedBalance(address);
}
@Post('/nickname')
@ApiOperation({ summary: '', description: '' })
setNodeNickname(@Body() body: setNicknameDto): any {
return this.nodeService.setNodeNickname(body.address, body.nickname);
}
@Get('/nickname/:nickname')
@ApiOperation({ summary: '', description: '' })
getNodeNickname(@Param('nickname') nickname: string): any {
return this.nodeService.checkNodeNickname(nickname);
}
@Get('/check-nickname/:nickname')
@ApiOperation({ summary: '', description: '' })
checkNickname(@Param('nickname') nickname: string): any {
return this.nodeService.getNodeNickname(nickname);
}
@Get('/:address')
@ApiOperation({ summary: '', description: '' })
//@Param('id') id: number
getNode(@Param('address') address: string): any {
return this.nodeService.getNode(address);
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Patch('/:id')
@ApiOperation({ summary: '', description: '' })
patchNode(@Param('id') id: number, @Body() body: patchNodeDto): any {
return this.nodeService.patchNode(id, body);
}
@Post('/')
@ApiBody({})
@ApiOperation({ summary: '', description: '' })
@UseGuards(JwtAuthGuard)
createNode(@Request() req: any, @Body() body: createNodeDto): any {
return this.nodeService.createNode(req.user.userId, body.address);
}
@Post('/:address/refferal')
@ApiOperation({ summary: '', description: '' })
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
applyNodeReferral(
@Param('address') address: string,
@Request() req: any,
@Body() body: applyNodeRefferalDto,
): any {
return this.nodeService.applyNodeReffal(
req.user.userId,
address,
body.referralCode,
);
}
@Get('/:address/refferal')
@ApiOperation({ summary: '', description: '' })
//@Param('id') id: number
getNodeReferral(@Param('address') address: string): any {
return this.nodeService.getNodeReferral(address);
}
@Get('/')
@ApiOperation({ summary: '', description: '' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
getnode(@Request() req: any, @Query() query: NodeQueryDto): any {
var userId = req.user.userId;
return this.nodeService.getNodeQuery(userId, query);
}
@Delete('/:id')
@ApiOperation({ summary: '', description: '' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
deleteNode(@Request() req: any, @Param('id') id: number): any {
var userId = req.user.userId;
return this.nodeService.deleteNode(userId, id);
}
}

9
src/node/node.module.ts Normal file
View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { NodeService } from './node.service';
import { NodeController } from './node.controller';
@Module({
providers: [NodeService],
controllers: [NodeController],
})
export class NodeModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { NodeService } from './node.service';
describe('NodeService', () => {
let service: NodeService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [NodeService],
}).compile();
service = module.get<NodeService>(NodeService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

549
src/node/node.service.ts Normal file
View File

@@ -0,0 +1,549 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma.service';
import { Node, Prisma } from '@prisma/client';
import { generateRandomString } from '../utils/randomstring';
import { createNodeDto, patchNodeDto } from './node.controller';
import NodeQueryDto from './dto/node.query.dto';
import { redis } from '../redis';
import { ethers } from 'ethers';
import { updateActive } from '../jsonrpc';
import { Decimal } from '@prisma/client/runtime/library';
const QUERY = function (nodeId, depth) {
return `
WITH RECURSIVE Subtree AS (
SELECT
id,
address,
balance,
stakedBalance,
depositedBalance,
computingPower,
childrenAccumulatedBalance,
childrenAccumulatedStakedBalance,
childrenAccumulatedCount,
childrenAccumulatedActiveCount,
parentId,
isActivated,
nickname,
referral,
depth,
childrenDepth,
0 AS qdepth
FROM
Node
WHERE
id = ${nodeId}
UNION ALL
SELECT
n.id,
n.address,
n.balance,
n.stakedBalance,
n.depositedBalance,
n.computingPower,
n.childrenAccumulatedBalance,
n.childrenAccumulatedStakedBalance,
n.childrenAccumulatedCount,
n.childrenAccumulatedActiveCount,
n.parentId,
n.isActivated,
n.nickname,
n.referral,
n.depth,
n.childrenDepth,
st.qdepth + 1 AS qdepth
FROM
Node n
INNER JOIN
Subtree st ON n.parentId = st.id
WHERE
st.qdepth < ${depth}
)
SELECT
id,
address,
balance,
depositedBalance,
stakedBalance,
computingPower,
childrenAccumulatedBalance,
childrenAccumulatedStakedBalance,
childrenAccumulatedActiveCount,
childrenAccumulatedCount,
parentId,
isActivated,
nickname,
referral,
depth,
childrenDepth
FROM
Subtree
WHERE
qdepth > 0;
`;
};
const MAX_DEPTH_QUERY = function (nodeId) {
return `
WITH RECURSIVE NodeHierarchy AS (
SELECT
id,
parentId,
0 AS depth
FROM
Node
WHERE
id = ${nodeId}
UNION ALL
SELECT
n.id,
n.parentId,
nh.depth + 1 AS depth
FROM
Node n
INNER JOIN
NodeHierarchy nh ON n.parentId = nh.id
)
SELECT
MAX(depth) AS max_depth,
COUNT(*) AS total_children_count
FROM
NodeHierarchy;
`;
};
const NODE_LEVEL_QUERY = `
WITH ranking_list as (
SELECT
n.address,
n.stakedBalance,
RANK() OVER(ORDER BY stakedBalance) as ranking
FROM
Node n
WHERE
n.stakedBalance >= 200000000000000000000
ORDER BY stakedBalance DESC
) SELECT
ranking
FROM
ranking_list
WHERE
address = ?;
`;
const NODE_LEVEL_LIST_QUERY = `
WITH ranking_list as (
SELECT
n.address,
n.stakedBalance,
n.computingPower,
n.childrenAccumulatedBalance,
RANK() OVER(ORDER BY stakedBalance) as ranking
FROM
Node n
WHERE
stakedBalance >= 200000000000000000000
ORDER BY stakedBalance DESC
) SELECT
address,
ranking,
stakedBalance,
computingPower,
childrenAccumulatedBalance
FROM
ranking_list;
`;
const NODE_TOTAL_LEVEL_SUM_QUERY = `
WITH ranking_list as (
SELECT
n.address,
n.stakedBalance,
RANK() OVER(ORDER BY stakedBalance) as ranking
FROM
Node n
WHERE
n.stakedBalance >= 200000000000000000000
ORDER BY stakedBalance DESC
) SELECT
SUM(ranking)
FROM
ranking_list;
`;
const NODE_TOTAL_COMPUTINGPOWER_SUM_QUERY = `
SELECT
SUM(computingPower)
FROM
Node;
WHERE
isActivated = true AND stakedBalance >= 200000000000000000000;
`;
// const NODE_HEIGHT_QUERY = ( id : number) => `
// WITH RECURSIVE NodeHierarchy AS (
// SELECT id, parentId, 1 AS height
// FROM Node
// WHERE id = ${id}
// UNION ALL
// SELECT n.id, n.parentId, nh.height + 1
// FROM NodeHierarchy nh
// JOIN Node n ON nh.id = n.parentId
// )
// SELECT id, height
// FROM NodeHierarchy
// WHERE id <> ${id};
// `
let checkNodes = [];
@Injectable()
export class NodeService {
async getNeedCheckNode(address: string): Promise<any> {
checkNodes.push(address);
const remove = await updateActive(checkNodes);
console.log('remove : ', remove);
checkNodes = checkNodes.filter((item) => !remove.includes(item));
return remove;
}
async findByReferral(code: string): Promise<any> {
console.log('code : ', code);
return await this.prisma.node.findFirstOrThrow({
where: { referral: code },
});
}
async deleteNode(userId: string, id: number): Promise<any> {
return await this.prisma.node.update({
where: {
id,
},
data: {
userId: null,
},
});
}
async getNodeDepth() {}
async patchNode(id: number, body: patchNodeDto): Promise<any> {
return await this.prisma.node.update({
where: {
id,
},
data: {
nickname: body.nickname,
},
});
}
async applyNodeReffal(
userId: any,
address: any,
referralCode: any,
): Promise<any> {
const node = await this.prisma.node.findFirstOrThrow({
where: {
userId,
address,
},
});
console.log('node : ', node);
console.log('node : ', referralCode);
const referralNode = await this.prisma.node.findFirstOrThrow({
where: {
referral: referralCode,
},
});
console.log('referralNode : ', referralNode);
if (node.parentId) {
throw new Error('Already applied referral');
}
if (node.address === referralNode.address) {
throw new Error('You cannot apply your own referral code');
}
console.log('referralNode', referralNode.id);
console.log('node', node.id);
console.log('referralNode', referralNode.depth);
return await this.prisma.node.update({
where: {
id: node.id,
},
data: {
parent: {
connect: {
id: referralNode.id,
},
},
depth: referralNode.depth + 1,
},
});
}
constructor(private prisma: PrismaService) {}
async getNodeByAddress(address: string): Promise<Object | null> {
const { value: dividnece } = await this.prisma.system.findUnique({
where: { key: 'distribution-amount' },
});
const node = await this.prisma.node.findFirst({
where: { address: address },
include: {
children: true,
},
});
return node;
}
async updateChildrenDepth(): Promise<any> {
const nodes = await this.prisma.node.findMany({
orderBy: {
depth: 'desc',
},
take: 1,
include: {
parent: true,
},
});
const node = nodes[0];
node.childrenDepth = 0;
node.parent.childrenDepth = node.childrenDepth + 1;
}
async setReferral(
address: string,
referral: string,
checksum: string,
): Promise<Object> {
const updatedNode = await this.prisma.node.updateMany({
where: { address: { equals: address } },
data: { referral: referral },
});
if (updatedNode.count > 0) {
return { isSet: true };
} else {
return { isSet: false };
}
}
async getNodeComputingPower(address: string): Promise<number> {
const node = await this.prisma.node.findUnique({
where: { address: address },
});
return (
Math.floor(
node.computingPower.div(new Decimal(10).pow(18)).toNumber() * 100,
) / 100 || 0
);
}
//
async getChildrenStakedBalance(address: string): Promise<number> {
const node = await this.prisma.node.findUnique({
where: { address: address },
});
return (
Math.floor(
node.childrenAccumulatedBalance
.div(new Decimal(10).pow(18))
.toNumber() * 100,
) / 100 || 0
);
}
async getNodeActiveCount(address: string): Promise<number> {
const node = await this.prisma.node.findUnique({
where: { address: address },
});
return node.childrenAccumulatedActiveCount.toNumber() || 0;
}
async getNodeDeactiveCount(address: string): Promise<number> {
const node = await this.prisma.node.findUnique({
where: { address: address },
});
return node.childrenAccumulatedDeactiveCount.toNumber() || 0;
}
async getNodeLevel(address: string) {
const res = await this.prisma.$queryRawUnsafe(NODE_LEVEL_QUERY, address);
//console.log(res)
return res[0]?.ranking || 0;
}
async getNodeLevelList() {
const res: [] = await this.prisma.$queryRawUnsafe(NODE_LEVEL_LIST_QUERY);
//console.log(res)
return res.map((node: any) => {
node.stakedBalance = BigInt(node.stakedBalance.toNumber());
node.computingPower = BigInt(node.computingPower.toNumber());
node.childrenAccumulatedBalance = BigInt(
node.childrenAccumulatedBalance.toNumber(),
);
return node;
});
}
async getNodeRank(address: string): Promise<number | null> {
return await this.getNodeLevel(address);
}
async getNodeGraph(address: string, depth: number): Promise<Node | null> {
return this.prisma.node.findFirst({
where: { address: address },
include: { children: true, parent: true },
});
}
async setNodeNickname(address: string, nickname: string): Promise<boolean> {
const first = await this.prisma.node.findFirst({
where: { address: address },
});
if (!first || !first.nickname) {
const res = await this.prisma.node.updateMany({
where: { address: address },
data: { nickname: nickname },
});
return true;
} else {
return false;
}
}
async getNodeNickname(address: string): Promise<string | null> {
const node = await this.prisma.node.findFirst({
where: { address: address },
select: { nickname: true },
});
return node?.nickname ?? null;
}
async checkNodeNickname(nickname: string): Promise<Object> {
const node = await this.prisma.node.findFirst({
where: { nickname: nickname },
});
if (node) {
return { isNickname: true, address: node.address ?? '' };
} else {
return { isNickname: false };
}
}
async getNodeList(props: {
nickname?: string;
referralCode?: string;
}): Promise<any> {
return await this.prisma.node.findMany({
where: {
nickname: props.nickname,
referral: props.referralCode,
},
});
}
async getNodeQuery(userId: string, props: NodeQueryDto): Promise<any> {
var { address, nickname, depth, status, myAddress } = props;
status =
typeof status === 'undefined'
? undefined
: status === 'true'
? true
: false;
//console.log("props : ", props);
var t = await this.prisma.node.findUnique({
where: { address: myAddress },
});
var maxCount = await this.prisma.$queryRawUnsafe(MAX_DEPTH_QUERY(t.id));
var data = await this.prisma.$queryRawUnsafe<any[]>(
QUERY(t.id, depth ?? 100),
);
data = [
{
...t,
isActivated: t.isActivated ? 1 : 0,
},
...data,
];
if (typeof status !== 'undefined') {
data = data.filter((node) => {
return status == 1 ? node.isActivated : !node.isActivated;
});
}
if (nickname) {
data = data.filter((node) => {
console.log(node.nickname);
return node.nickname?.includes(nickname);
});
}
if (address) {
data = data.filter((node) => {
console.log(node.address);
return node.address.includes(address);
});
}
return {
maxCount,
data,
};
}
async getNode(address: string): Promise<Node | null> {
return this.prisma.node.findFirst({
where: { address: address },
include: {
children: true,
},
});
}
async createNode(userId: string, address: string): Promise<Node | null> {
const data: Prisma.NodeCreateInput = {
address,
referral: generateRandomString(7).toUpperCase(),
User: {
connect: {
userId: userId,
},
},
};
return await this.prisma.node.upsert({
where: { address: address },
update: {
address: data.address,
User: data.User,
},
create: data,
});
}
async getNodeReferral(address: string): Promise<any> {
const node = await this.prisma.node.findFirstOrThrow({
where: { address: address },
});
return node;
}
/*
async setNodeParent(address: string, parent: string) {
const node = await this.prisma.node.updateMany({where:{ address: address}})
}
async setNodeChildren(address: string, children: string[]) {
}
*/
}

View File

@@ -0,0 +1,17 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { Node2AddressService } from './node2address.service';
@Controller('node2address')
export class Node2AddressController {
constructor(private readonly node2AddressService: Node2AddressService) {}
@Get('/:userId')
getHello(@Param('userId') userId: string) {
return this.node2AddressService.getN2A(userId);
}
@Post('/:userId')
writeN2A(@Param('userId') userId: string, @Body('address') body: any) {
return this.node2AddressService.writeN2A(userId, body);
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { Node2AddressService } from './node2address.service';
import { Node2AddressController } from './node2address.controller';
@Module({
providers: [Node2AddressService],
controllers: [Node2AddressController],
})
export class Node2AddressModule {}

View File

@@ -0,0 +1,31 @@
import { Get, Injectable } from '@nestjs/common';
import { skip } from 'node:test';
import { PrismaService } from 'src/prisma.service';
@Injectable()
export class Node2AddressService {
static getN2A(userId: string) {
throw new Error('Method not implemented.');
}
constructor(private readonly prisma: PrismaService) {}
async getN2A(userId: string) {
return await this.prisma.userToAddressBackup.findMany({
where: {
userId,
},
});
}
async writeN2A(userId: string, address: string[]) {
return await this.prisma.userToAddressBackup.createMany({
data: address.map((address) => {
return {
userId,
address,
};
}),
skipDuplicates: true,
});
}
}

9
src/prisma.module.ts Normal file
View File

@@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

9
src/prisma.service.ts Normal file
View File

@@ -0,0 +1,9 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
}

11
src/redis.ts Normal file
View File

@@ -0,0 +1,11 @@
import Redis from 'ioredis';
const host = process.env.REDIS_HOST || '127.0.0.1';
const port = process.env.REDIS_PORT ? Number(process.env.REDIS_PORT) : 6379;
const password = process.env.REDIS_PASSWORD || undefined;
export const redis = new Redis({
host,
port,
password,
});

221
src/schema.dbml Normal file
View File

@@ -0,0 +1,221 @@
//// ------------------------------------------------------
//// THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
//// ------------------------------------------------------
Project "BKON Wallet Schema" {
database_type: 'mysql'
Note: 'BKON Wallet Distributor Table'
}
Table Board {
id Int [pk, increment]
title String
body String
imgUrl String
count Int [not null, default: 0]
categoryId Int
createdAt DateTime [default: `now()`, not null]
updatedAt DateTime [default: `now()`, not null]
link String
category Category
}
Table Category {
id Int [pk, increment]
name String [not null]
createdAt DateTime [default: `now()`, not null]
updatedAt DateTime [default: `now()`, not null]
Board Board [not null]
}
Table Node {
id Int [pk, increment]
address String [unique, not null]
balance Decimal [not null, default: 0]
stakedBalance Decimal [not null, default: 0]
computingPower Decimal [not null, default: 0]
parentId Int
isActivated Boolean [not null, default: false]
nickname String
referral String [unique, not null]
createdAt DateTime [default: `now()`, not null]
updatedAt DateTime [default: `now()`, not null]
userId String
childrenAccumulatedBalance Decimal [not null, default: 0]
childrenAccumulatedCount Decimal [not null, default: 0]
childrenAccumulatedActiveCount Decimal [not null, default: 0]
childrenAccumulatedDeactiveCount Decimal [not null, default: 0]
childrenAccumulatedStakedBalance Decimal [not null, default: 0]
childrenDepth Int [not null, default: 0]
depth Int [not null, default: 0]
depositedBalance Decimal [not null, default: 0]
realName String
parent Node
children Node [not null]
User User
}
Table Txes {
id Int [pk, increment]
txid String [not null]
from String [not null]
to String [not null]
value Decimal [not null, default: 0]
fee Decimal [not null, default: 0]
internalFee Decimal [not null, default: 0]
type String [not null]
blockTimestamp DateTime [not null]
createdAt DateTime [default: `now()`, not null]
updatedAt DateTime [default: `now()`, not null]
}
Table User {
userId String [pk]
IMEI String
mac String
nickname String
fcmToken String
createdAt DateTime [default: `now()`, not null]
updatedAt DateTime [default: `now()`, not null]
pushAllowAt DateTime
language String
Node Node [not null]
favoriteAddress favoriteAddress [not null]
userNotification userNotification [not null]
}
Table favoriteAddress {
address String [not null]
createdAt DateTime [default: `now()`, not null]
updatedAt DateTime [default: `now()`, not null]
userId String [not null]
nickname String
User User [not null]
indexes {
(address, userId) [pk]
}
}
Table userNotification {
id String [pk]
title String [not null]
body String [not null]
data Json [not null]
userId String [not null]
type NotificationType [not null]
createdAt DateTime [default: `now()`, not null]
updatedAt DateTime [default: `now()`, not null]
link String [not null]
user User [not null]
}
Table userToAddressBackup {
userId String [not null]
address String [not null]
createdAt DateTime [default: `now()`, not null]
updatedAt DateTime [default: `now()`, not null]
indexes {
(userId, address) [pk]
}
}
Table System {
id Int [pk, increment]
key String [unique, not null]
value String [not null]
createdAt DateTime [default: `now()`, not null]
updatedAt DateTime [default: `now()`, not null]
}
Table snapshot {
id Int [pk]
address String [unique, not null]
nickname String
parentId Int
stakedBalance Decimal
computingPower Decimal
childrenAccumulatedBalance Decimal
isActivated Boolean [not null]
balance Decimal
childrenAccumulatedCount Decimal
childrenAccumulatedDeactiveCount Decimal
childrenAccumulatedStakedBalance Decimal
Txid String
isCPSent Boolean [not null]
isRankSent Boolean [not null]
isCPSendStarted Boolean [not null]
isRankSendStarted Boolean [not null]
}
Table distribution {
id Int [pk]
nickname String
address String
parentId Int
ranking BigInt
stakedBalance Float
computingPower Float
childrenAccumulatedBalance Float
stakedBalanceOrig Decimal
computingPowerOrig Decimal
childrenAccumulatedBalanceOrig Decimal
ranking_portion Decimal
staked_portion Decimal
computingPower_portion Decimal
childrenAccumulatedBalance_portion Decimal
ranking_estimated_dist Decimal
computingPower_estimated_dist Decimal
Txid String
isCPSent Boolean [not null]
isRankSent Boolean [not null]
isCPSendStarted Boolean [not null]
isRankSendStarted Boolean [not null]
}
Table LockedCoin {
id String [pk]
address String [not null]
amount Decimal [not null, default: 0]
createdAt DateTime [default: `now()`, not null]
updatedAt DateTime [default: `now()`, not null]
unlocked Boolean [not null, default: false]
unlockAt DateTime
reason String
txId String
sendedAt DateTime
}
Table TransferHistories {
idx BigInt [pk, increment]
txhash String [not null]
from_address String [not null]
to_address String [not null]
amount Decimal [not null]
memo String
timestamp DateTime [default: `now()`, not null]
createdAt DateTime [default: `now()`, not null]
log_index BigInt [not null]
blockNumber BigInt [not null]
indexes {
(txhash, log_index, timestamp) [unique]
}
}
Enum NotificationType {
SYSTEM
USER
TRANSACTION
}
Ref: Board.categoryId > Category.id
Ref: Node.parentId - Node.id
Ref: Node.userId > User.userId
Ref: favoriteAddress.userId > User.userId
Ref: userNotification.userId > User.userId

18
src/style.css Normal file
View File

@@ -0,0 +1,18 @@
body {
font-family: helvetica, sans-serif;
font-size: 14px;
}
#cy {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 999;
}
h1 {
opacity: 0.5;
font-size: 1em;
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SystemController } from './system.controller';
describe('SystemController', () => {
let controller: SystemController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [SystemController],
}).compile();
controller = module.get<SystemController>(SystemController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,156 @@
import { Controller, Get, Post, Param, Body } from '@nestjs/common';
import {
ApiCreatedResponse,
ApiOperation,
ApiTags,
ApiBody,
ApiProperty,
} from '@nestjs/swagger';
import { PrismaService } from '../prisma.service';
import { ethers } from 'ethers';
class system {
@ApiProperty()
bkonUsdPrice: number;
@ApiProperty()
transferFee: number;
@ApiProperty()
activationFee: number;
@ApiProperty()
dividend: number;
}
class price {
@ApiProperty()
bkonUsdPrice: number;
}
class fee {
@ApiProperty()
transferFee: number;
@ApiProperty()
activationFee: number;
@ApiProperty()
stakingFee: number;
@ApiProperty()
unstakingFee: number;
}
class systemwallet {
@ApiProperty()
distributionAddress: string;
@ApiProperty()
dividendPoolAddress: string;
}
class dividend {
@ApiProperty()
dividend: number;
}
@Controller({
path: 'system',
version: '1',
})
@ApiTags('시스템 API')
export class SystemController {
constructor(private readonly prisma: PrismaService) {}
@Get('/')
@ApiOperation({
summary: '시스템 정보 가져오기',
description: '가격/수수료/배당액을 가져온다',
})
async getAll(): Promise<system> {
const { value: activationFee } = await this.prisma.system.findUnique({
where: { key: 'fee-enable-wallet' },
});
const { value: transferFee } = await this.prisma.system.findUnique({
where: { key: 'fee-transfer' },
});
const { value: bkonUsdPice } = await this.prisma.system.findUnique({
where: { key: 'bkon-price' },
});
const { value: distributionAmount } = await this.prisma.system.findUnique({
where: { key: 'distribution-amount' },
});
return {
bkonUsdPrice: parseFloat(bkonUsdPice),
transferFee: parseFloat(ethers.formatEther(transferFee)),
activationFee: parseFloat(ethers.formatEther(activationFee)),
dividend: parseFloat(ethers.formatEther(distributionAmount)),
};
}
@Get('/price')
@ApiOperation({
summary: '가격 정보 가져오기',
description: 'BKON 가격 정보를 가져오기',
})
async getPrice(): Promise<price> {
const { value: bkonUsdPice } = await this.prisma.system.findUnique({
where: { key: 'bkon-price' },
});
return {
bkonUsdPrice: parseFloat(bkonUsdPice),
};
}
@Get('/fee')
@ApiOperation({
summary: '수수료 정보 가져오기',
description: 'BKON 작업 관련 수수료에 대해서 받아온다',
})
async getFee(): Promise<fee> {
const { value: activationFee } = await this.prisma.system.findUnique({
where: { key: 'fee-enable-wallet' },
});
const { value: transferFee } = await this.prisma.system.findUnique({
where: { key: 'fee-transfer' },
});
const { value: stakingFee } = await this.prisma.system.findUnique({
where: { key: 'fee-staking' },
});
const { value: unstakingFee } = await this.prisma.system.findUnique({
where: { key: 'fee-unstaking' },
});
return {
transferFee: parseFloat(ethers.formatEther(transferFee)),
activationFee: parseFloat(ethers.formatEther(activationFee)),
stakingFee: parseFloat(ethers.formatEther(stakingFee)),
unstakingFee: parseFloat(ethers.formatEther(unstakingFee)),
};
}
@Get('/system-wallet')
@ApiOperation({
summary: '수수료 정보 가져오기',
description: 'BKON 작업 관련 수수료에 대해서 받아온다',
})
async getWallet(): Promise<systemwallet> {
const { value: distributionAddress } = await this.prisma.system.findUnique({
where: { key: 'distribution-address' },
});
const { value: dividendPoolAddress } = await this.prisma.system.findUnique({
where: { key: 'dividend-pool-address' },
});
return {
distributionAddress: distributionAddress,
dividendPoolAddress: dividendPoolAddress,
};
}
@Get('/dividend')
@ApiOperation({
summary: '배당 정보 가져오기',
description: '배당액에 대해서 받아온다',
})
async getDividend(): Promise<dividend> {
const { value: distributionAmount } = await this.prisma.system.findUnique({
where: { key: 'distribution-amount' },
});
return {
dividend: parseFloat(ethers.formatEther(distributionAmount)),
};
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { SystemService } from './system.service';
import { SystemController } from './system.controller';
@Module({
providers: [SystemService],
controllers: [SystemController],
})
export class SystemModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SystemService } from './system.service';
describe('SystemService', () => {
let service: SystemService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [SystemService],
}).compile();
service = module.get<SystemService>(SystemService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class SystemService {}

View File

@@ -0,0 +1,35 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsBoolean,
IsEmpty,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
} from 'class-validator';
export default class TransferQueryDto {
@IsOptional()
@IsString()
@ApiProperty({
nullable: true,
required: false,
})
readonly address?: string;
@IsOptional()
@IsString()
@ApiProperty({
nullable: true,
required: false,
})
readonly reason?: string;
@IsOptional()
@IsNumber()
@ApiProperty({
nullable: true,
required: false,
})
readonly page?: number;
}

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TranferHistoriesController } from './transferhhistories.controller';
import { TranferHistoriesService } from './transferhistories.service';
describe('TranferHistoriesController', () => {
let controller: TranferHistoriesController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [TranferHistoriesController],
providers: [TranferHistoriesService],
}).compile();
controller = module.get<TranferHistoriesController>(
TranferHistoriesController,
);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TranferHistoriesService } from './transferhistories.service';
describe('TranferHistoriesService', () => {
let service: TranferHistoriesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [TranferHistoriesService],
}).compile();
service = module.get<TranferHistoriesService>(TranferHistoriesService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,51 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
ParseIntPipe,
Query,
} from '@nestjs/common';
import { TranferHistoriesService } from './transferhistories.service';
import TransferQueryDto from './dto/transferhistories.query.dto';
import { ApiOperation } from '@nestjs/swagger';
@Controller('transferlogs')
export class TranferHistoriesController {
constructor(
private readonly tranferHistoriesService: TranferHistoriesService,
) {}
@Get('/listAll')
@ApiOperation({
summary: '로그 전체 ',
description: '트랜젝션 로그 전체 리스트',
})
getListAll() {
return this.tranferHistoriesService.findAll();
}
@Get('/list')
getListPaging(@Query('page', ParseIntPipe) page: number) {
const currentPage = Number.isFinite(page) && page > 0 ? page : 1;
return this.tranferHistoriesService.findPageList(currentPage);
}
@Get('/list/filter')
getListFilterPaging(
@Query('page') page: string,
@Query('address') address: string,
@Query('reason') reason: string,
@Query('timestamp') timestamp: string,
) {
return this.tranferHistoriesService.findListPaging(
page,
address,
reason,
timestamp,
);
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TranferHistoriesService } from './transferhistories.service';
import { TranferHistoriesController } from './transferhhistories.controller';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule],
controllers: [TranferHistoriesController],
providers: [TranferHistoriesService],
})
export class TranferHistoriesModule {}

View File

@@ -0,0 +1,90 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma.service';
import TransferQueryDto from './dto/transferhistories.query.dto';
import { ConfigService } from '@nestjs/config';
import { formatUnits } from 'ethers';
import { Prisma } from '@prisma/client';
import {
TRANSFERLOGS_QUERY,
TRANSFERLOGS_UNIONALL_QUERY,
} from './transferhistories.sql';
@Injectable()
export class TranferHistoriesService {
private pageSize: number;
constructor(
private prisma: PrismaService,
private configService: ConfigService,
) {
this.pageSize = Number(this.configService.get<number>('PAGE_SIZE', 50));
}
async findAll() {
return await this.prisma.transferHistories.findMany({
orderBy: { idx: 'desc' },
});
}
async findPageList(page: number) {
return await this.prisma.transferHistories.findMany({
orderBy: { idx: 'desc' },
take: this.pageSize,
skip: ((page ?? 1) - 1) * this.pageSize,
});
}
async findList(body: TransferQueryDto) {
const where: any = {};
if (body.reason) {
where.memo = body.reason;
}
if (body.address) {
where.OR = [
{ from_address: { contains: body.address } },
{ to_address: { contains: body.address } },
];
}
return await this.prisma.transferHistories.findMany({
where,
orderBy: { idx: 'desc' },
});
}
async findListPaging(
page: string,
address: string,
reason: string,
timestamp: string,
) {
const parsed = page ? parseInt(page, 10) : NaN;
const currentPage =
!page || Number.isNaN(parsed) || parsed <= 0 ? 1 : parsed;
const take = this.pageSize;
const skip = (currentPage - 1) * take;
const ts = timestamp ? new Date(timestamp) : undefined;
let rows = [];
if (!address) {
rows = await this.prisma.$queryRaw(
TRANSFERLOGS_QUERY(reason, ts, take, skip),
);
} else {
rows = await this.prisma.$queryRaw(
TRANSFERLOGS_UNIONALL_QUERY(reason, address, ts, take, skip),
);
}
const mapped = rows.map((r) => ({
...r,
amount: parseFloat(formatUnits(r.amount.toFixed(0), 18)).toString(),
}));
return mapped;
}
}

View File

@@ -0,0 +1,89 @@
import { Prisma } from '@prisma/client';
export const TRANSFERLOGS_UNIONALL_QUERY = (
reason: string,
address: string,
ts: Date,
take: number,
skip: number,
) => {
return Prisma.sql`
SELECT
idx,
txhash,
blockNumber,
log_index,
from_address,
to_address,
amount,
memo,
timestamp,
createdAt
FROM (
SELECT
idx,
txhash,
blockNumber,
log_index,
from_address,
to_address,
amount,
memo,
timestamp,
createdAt
FROM TransferHistories
WHERE 1=1
${reason ? Prisma.sql`AND memo = ${reason}` : Prisma.empty}
${address ? Prisma.sql`AND from_address = ${address}` : Prisma.empty}
${ts ? Prisma.sql`AND timestamp >= ${ts}` : Prisma.empty}
UNION ALL
SELECT
idx,
txhash,
blockNumber,
log_index,
from_address,
to_address,
amount,
memo,
timestamp,
createdAt
FROM TransferHistories
WHERE 1=1
${reason ? Prisma.sql`AND memo = ${reason}` : Prisma.empty}
${address ? Prisma.sql`AND to_address = ${address}` : Prisma.empty}
${ts ? Prisma.sql`AND timestamp >= ${ts}` : Prisma.empty}
) AS t
ORDER BY t.idx DESC
LIMIT ${take} OFFSET ${skip}
`;
};
export const TRANSFERLOGS_QUERY = (
reason: string,
ts: Date,
take: number,
skip: number,
) => {
return Prisma.sql`
SELECT
idx,
txhash,
blockNumber,
log_index,
from_address,
to_address,
amount,
memo,
timestamp,
createdAt
FROM TransferHistories
WHERE 1=1
${reason ? Prisma.sql`AND memo = ${reason}` : Prisma.empty}
${ts ? Prisma.sql`AND timestamp >= ${ts}` : Prisma.empty}
ORDER BY idx DESC
LIMIT ${take} OFFSET ${skip}
`;
};

View File

@@ -0,0 +1,34 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmpty, IsNotEmpty, IsOptional, IsString } from 'class-validator';
export default class CreateUserReq {
@IsString()
@IsNotEmpty()
@ApiProperty()
readonly address: string;
@IsString()
@IsOptional()
@ApiProperty()
readonly nickname: string;
@IsString()
@IsOptional()
@ApiProperty()
readonly referralCode: string;
@IsString()
@IsOptional()
@ApiProperty()
readonly IMEI: string;
@IsString()
@IsOptional()
@ApiProperty()
readonly mac: string;
@IsString()
@IsOptional()
@ApiProperty()
readonly pushAllowAt: string;
}

View File

@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmpty, IsNotEmpty, IsOptional, IsString } from 'class-validator';
export default class FavorieterReq {
@IsString()
@ApiProperty()
readonly favorite: string;
@IsString()
@ApiProperty()
@IsOptional()
readonly nickname: string;
}

View File

@@ -0,0 +1,34 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmpty, IsNotEmpty, IsOptional, IsString } from 'class-validator';
export default class UpdateUserReq {
@IsString()
@IsOptional()
@ApiProperty()
readonly nickname: string;
@IsString()
@IsOptional()
@ApiProperty()
readonly IMEI: string;
@IsString()
@IsOptional()
@ApiProperty()
readonly mac: string;
@IsString()
@IsOptional()
@ApiProperty()
readonly fcmToken: string;
@IsString()
@IsOptional()
@ApiProperty()
readonly pushAllowAt: string;
@IsString()
@IsOptional()
@ApiProperty()
language: string;
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserController } from './user.controller';
describe('UserController', () => {
let controller: UserController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UserController],
}).compile();
controller = module.get<UserController>(UserController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

197
src/user/user.controller.ts Normal file
View File

@@ -0,0 +1,197 @@
import {
Controller,
Get,
Post,
Param,
Body,
Headers,
Query,
Put,
Delete,
Patch,
UseGuards,
Request,
} from '@nestjs/common';
import {
ApiCreatedResponse,
ApiOperation,
ApiTags,
ApiBody,
ApiProperty,
ApiHeader,
ApiQuery,
ApiBearerAuth,
} from '@nestjs/swagger';
import CreateUserReq from './dto/create.user.req';
import { UserService } from './user.service';
import { get } from 'http';
import { PrismaService } from '../prisma.service';
import FavorieterReq from './dto/favorite.req';
import UpdateUserReq from './dto/update.user.req';
import { JwtAuthGuard } from '../middleware/jwt.auth.guard';
import { AuthService } from '../auth/auth.service';
import { User } from '@prisma/client';
import { JwtService } from '@nestjs/jwt';
@Controller({
path: 'user',
version: '1',
})
@ApiTags('유저 API')
export class UserController {
constructor(
readonly userService: UserService,
readonly authService: AuthService,
readonly jwtService: JwtService,
private readonly prisma: PrismaService,
) {}
@ApiProperty()
@Post()
@ApiOperation({ summary: 'Create user', description: 'Create a new user' })
@ApiCreatedResponse({
description: 'The user has been successfully created.',
})
async createUser(@Body() body: CreateUserReq): Promise<any> {
const data: any = await this.userService.createUser(body);
const accessToken = await this.authService.getToken(data.userId);
const refreshToken = this.jwtService.sign(
{
time: new Date().getTime(),
},
{
secret: 'MNCOWINS1010!!!!',
},
);
return {
data,
accessToken,
refreshToken,
};
}
@Get('/referral-list')
@ApiOperation({ summary: 'get my Referral', description: '' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async getRefferalList(@Request() req: any): Promise<any> {
return await this.userService.getReffalUser(req.user.userId);
}
@Get()
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async getUser(@Request() req: any): Promise<any> {
return await this.userService.findUserById(req.user.userId);
}
@Get('exist')
@ApiOperation({
summary: 'find user',
description: '해당 유저가 존재 하면 true, 아님 false (AND연산)',
})
@ApiQuery({
name: 'address',
example: '0x1111111',
required: false,
})
@ApiQuery({
name: 'nickname',
example: 'nickname',
required: false,
})
@ApiQuery({
name: 'referralCode',
example: 'referralCode',
required: false,
})
async findUser(
@Query('address') address?: string,
@Query('nickname') nickname?: string,
@Query('referralCode') referralCode?: string,
): Promise<any> {
const user = await this.prisma.user.findFirst({
where: {
nickname,
Node:
address || referralCode
? {
some: {
address,
referral: referralCode,
},
}
: undefined,
},
});
return {
hasDuplicate: !!user,
};
}
@Patch()
@ApiOperation({ summary: 'update user', description: '' })
@ApiBody({
type: UpdateUserReq,
})
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async updateUser(
@Request() req: any,
@Body() body: UpdateUserReq,
): Promise<any> {
return await this.userService.updateUser(req.user.userId, body);
}
@Put('favorite')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'add favorite', description: '' })
@ApiProperty()
async addFavorite(
@Request() req: any,
@Body() body: FavorieterReq,
): Promise<any> {
return await this.userService.addFavorite(
req.user.userId,
body.favorite,
body.nickname,
);
}
@Delete('favorite')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async deleteFavorite(@Request() req: any, @Body() body: FavorieterReq) {
return await this.userService.deleteFavorite(
req.user.userId,
body.favorite,
);
}
@Get('favorite')
@ApiOperation({ summary: 'get favorite', description: '' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async getFavorite(@Request() req: any): Promise<any> {
return await this.userService.getFavorite(req.user.userId);
}
@Get('notification')
@ApiOperation({ summary: 'get notification', description: '' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async getNotification(
@Request() req: any,
@Query('take') take: number,
@Query('skip') skip: number,
): Promise<any> {
return await this.userService.getUsernotification(req.user.userId, {
take,
skip,
});
}
}

7
src/user/user.module.ts Normal file
View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
@Module({
providers: [UserService],
})
export class UserModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UserService],
}).compile();
service = module.get<UserService>(UserService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

Some files were not shown because too many files have changed in this diff Show More