clone:legacy
This commit is contained in:
25
.dockerignore
Normal file
25
.dockerignore
Normal 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
3
.env.ci.example
Normal 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
23
.env.development
Normal 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
26
.env.docker.example
Normal 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
2
.env.dockerize.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
USER=
|
||||||
|
TOKEN=
|
||||||
29
.env.example
Normal file
29
.env.example
Normal 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
23
.env.production.example
Normal 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=Ee2g2MrPQzBoVePepj2TqwFATuVOf6ACe7RpQCnd
|
||||||
23
.env.production.gkod
Normal file
23
.env.production.gkod
Normal 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=Ee2g2MrPQzBoVePepj2TqwFATuVOf6ACe7RpQCnd
|
||||||
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal 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
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pnpm lint-staged
|
||||||
16
.prettierignore
Normal file
16
.prettierignore
Normal 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
10
.prettierrc.json
Normal 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
0
.vscode/settings.json
vendored
Normal file
96
Dockerfile
Normal file
96
Dockerfile
Normal 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
73
README.md
Normal 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>
|
||||||
|
<!--[](https://opencollective.com/nest#backer)
|
||||||
|
[](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
3
bin/updateCommitSha.sh
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
git rev-parse --short=6 HEAD > commit-sha.txt
|
||||||
13
bkon-wallet-firebase-adminsdk-i39eg-cee0810b56.json
Normal file
13
bkon-wallet-firebase-adminsdk-i39eg-cee0810b56.json
Normal 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
49
compose.local.yml
Normal 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
36
deploy.yml
Normal 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
18
depth.js
Normal 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
22
docker-compose.prod.yml
Normal 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
54
docker-compose.yml
Normal 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
6
docker-daemon.json
Normal 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
39
dockerize.yml
Normal 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
18
ecosystem.config.js
Normal 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
15
eslint.config.mjs
Normal 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
8
nest-cli.json
Normal 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
140
package.json
Normal 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
22912
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
229
prisma/schema.prisma
Normal file
229
prisma/schema.prisma
Normal 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
|
||||||
|
}
|
||||||
91
prisma/views/DKON/NodeDashboard.sql
Normal file
91
prisma/views/DKON/NodeDashboard.sql
Normal 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`
|
||||||
66
prisma/views/DKON/NodeDashboardAll.sql
Normal file
66
prisma/views/DKON/NodeDashboardAll.sql
Normal 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
50
src/admin/userlist.jsx
Normal 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;
|
||||||
22
src/app.controller.spec.ts
Normal file
22
src/app.controller.spec.ts
Normal 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
20
src/app.controller.ts
Normal 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
185
src/app.module.ts
Normal 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
25
src/app.service.ts
Normal 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
919
src/assets/defi-abi.ts
Normal 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
12
src/assets/erc20-abi.ts
Normal 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
50
src/assets/i18n.ts
Normal 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 } };
|
||||||
18
src/assets/multicall-abi.ts
Normal file
18
src/assets/multicall-abi.ts
Normal 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)',
|
||||||
|
];
|
||||||
26
src/assets/multisender-abi.ts
Normal file
26
src/assets/multisender-abi.ts
Normal 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
30
src/auth/auth.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/board/board.controller.spec.ts
Normal file
18
src/board/board.controller.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
132
src/board/board.controller.ts
Normal file
132
src/board/board.controller.ts
Normal 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/board/board.module.ts
Normal file
9
src/board/board.module.ts
Normal 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 {}
|
||||||
18
src/board/board.service.spec.ts
Normal file
18
src/board/board.service.spec.ts
Normal 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
595
src/board/board.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/components/DecimalComponent.tsx
Normal file
15
src/components/DecimalComponent.tsx
Normal 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
204
src/cron.ts
Normal 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),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
48
src/firebase/dto/push.req.dto.ts
Normal file
48
src/firebase/dto/push.req.dto.ts
Normal 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;
|
||||||
|
}
|
||||||
115
src/firebase/firebase.controller.spec.ts
Normal file
115
src/firebase/firebase.controller.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
91
src/firebase/firebase.controller.ts
Normal file
91
src/firebase/firebase.controller.ts
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/firebase/firebase.module.ts
Normal file
4
src/firebase/firebase.module.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({})
|
||||||
|
export class FirebaseModule {}
|
||||||
108
src/firebase/firebase.service.spec.ts
Normal file
108
src/firebase/firebase.service.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
62
src/firebase/firebase.service.ts
Normal file
62
src/firebase/firebase.service.ts
Normal 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
250
src/index.js
Normal file
File diff suppressed because one or more lines are too long
138
src/jsonrpc/index.ts
Normal file
138
src/jsonrpc/index.ts
Normal 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;
|
||||||
|
}
|
||||||
18
src/locked-coin/locked-coin.controller.spec.ts
Normal file
18
src/locked-coin/locked-coin.controller.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
17
src/locked-coin/locked-coin.controller.ts
Normal file
17
src/locked-coin/locked-coin.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/locked-coin/locked-coin.module.ts
Normal file
9
src/locked-coin/locked-coin.module.ts
Normal 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 {}
|
||||||
18
src/locked-coin/locked-coin.service.spec.ts
Normal file
18
src/locked-coin/locked-coin.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
49
src/locked-coin/locked-coin.service.ts
Normal file
49
src/locked-coin/locked-coin.service.ts
Normal 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
132
src/logging.ts
Normal 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
71
src/main.ts
Normal 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();
|
||||||
5
src/middleware/jwt.auth.guard.ts
Normal file
5
src/middleware/jwt.auth.guard.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
||||||
19
src/middleware/jwt.strategy.ts
Normal file
19
src/middleware/jwt.strategy.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/node/dto/node.query.dto.ts
Normal file
55
src/node/dto/node.query.dto.ts
Normal 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;
|
||||||
|
}
|
||||||
18
src/node/node.controller.spec.ts
Normal file
18
src/node/node.controller.spec.ts
Normal 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
289
src/node/node.controller.ts
Normal 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
9
src/node/node.module.ts
Normal 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 {}
|
||||||
18
src/node/node.service.spec.ts
Normal file
18
src/node/node.service.spec.ts
Normal 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
549
src/node/node.service.ts
Normal 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[]) {
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
17
src/node2address/node2address.controller.ts
Normal file
17
src/node2address/node2address.controller.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/node2address/node2address.module.ts
Normal file
9
src/node2address/node2address.module.ts
Normal 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 {}
|
||||||
31
src/node2address/node2address.service.ts
Normal file
31
src/node2address/node2address.service.ts
Normal 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
9
src/prisma.module.ts
Normal 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
9
src/prisma.service.ts
Normal 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
11
src/redis.ts
Normal 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
221
src/schema.dbml
Normal 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
18
src/style.css
Normal 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;
|
||||||
|
}
|
||||||
18
src/system/system.controller.spec.ts
Normal file
18
src/system/system.controller.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
156
src/system/system.controller.ts
Normal file
156
src/system/system.controller.ts
Normal 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)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/system/system.module.ts
Normal file
9
src/system/system.module.ts
Normal 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 {}
|
||||||
18
src/system/system.service.spec.ts
Normal file
18
src/system/system.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
4
src/system/system.service.ts
Normal file
4
src/system/system.service.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SystemService {}
|
||||||
35
src/transferhistories/dto/transferhistories.query.dto.ts
Normal file
35
src/transferhistories/dto/transferhistories.query.dto.ts
Normal 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;
|
||||||
|
}
|
||||||
22
src/transferhistories/tranferhistories.controller.spec.ts
Normal file
22
src/transferhistories/tranferhistories.controller.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
18
src/transferhistories/tranferhistories.service.spec.ts
Normal file
18
src/transferhistories/tranferhistories.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
51
src/transferhistories/transferhhistories.controller.ts
Normal file
51
src/transferhistories/transferhhistories.controller.ts
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/transferhistories/transferhistories.module.ts
Normal file
11
src/transferhistories/transferhistories.module.ts
Normal 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 {}
|
||||||
90
src/transferhistories/transferhistories.service.ts
Normal file
90
src/transferhistories/transferhistories.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
89
src/transferhistories/transferhistories.sql.ts
Normal file
89
src/transferhistories/transferhistories.sql.ts
Normal 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}
|
||||||
|
`;
|
||||||
|
};
|
||||||
34
src/user/dto/create.user.req.ts
Normal file
34
src/user/dto/create.user.req.ts
Normal 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;
|
||||||
|
}
|
||||||
12
src/user/dto/favorite.req.ts
Normal file
12
src/user/dto/favorite.req.ts
Normal 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;
|
||||||
|
}
|
||||||
34
src/user/dto/update.user.req.ts
Normal file
34
src/user/dto/update.user.req.ts
Normal 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;
|
||||||
|
}
|
||||||
18
src/user/user.controller.spec.ts
Normal file
18
src/user/user.controller.spec.ts
Normal 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
197
src/user/user.controller.ts
Normal 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
7
src/user/user.module.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { UserService } from './user.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [UserService],
|
||||||
|
})
|
||||||
|
export class UserModule {}
|
||||||
18
src/user/user.service.spec.ts
Normal file
18
src/user/user.service.spec.ts
Normal 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
Reference in New Issue
Block a user