clone:legacy

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

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

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

204
src/cron.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

250
src/index.js Normal file

File diff suppressed because one or more lines are too long

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

132
src/logging.ts Normal file
View File

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

71
src/main.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

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

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

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

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

11
src/redis.ts Normal file
View File

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

221
src/schema.dbml Normal file
View File

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

18
src/style.css Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

234
src/user/user.service.ts Normal file
View File

@@ -0,0 +1,234 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma.service';
import { User } from './user';
import CreateUserReq from './dto/create.user.req';
import UpdateUserReq from './dto/update.user.req';
import { userNotification } from '@prisma/client';
import { generateRandomString } from '../utils/randomstring';
import { AuthService } from '../auth/auth.service';
import { uuidV4 } from 'ethers';
@Injectable()
export class UserService {
async findUserById(userId: string) {
return await this.prisma.user.findUnique({
where: {
userId,
},
include: {
Node: {
include: {
parent: {},
children: {},
},
},
},
});
}
constructor(private readonly prisma: PrismaService) {}
async addFavorite(userId: string, address: string, nickname: string) {
return await this.prisma.favoriteAddress.upsert({
where: {
address_userId: {
userId,
address,
},
},
update: {
nickname,
},
create: {
userId,
address,
nickname,
},
});
}
async deleteFavorite(userId: string, address: string) {
return await this.prisma.favoriteAddress.delete({
where: {
address_userId: {
userId,
address,
},
},
});
}
async getFavorite(userId: string) {
return await this.prisma.favoriteAddress.findMany({
where: {
userId,
},
select: {
address: true,
createdAt: true,
},
});
}
async findByAddress(address: string): Promise<User> {
return await this.prisma.user.findFirst({
where: {
Node: {
some: {
address: address,
},
},
},
});
}
async createUser(data: CreateUserReq): Promise<User> {
const { address, nickname, referralCode, IMEI, mac, pushAllowAt } = data;
let referralUser;
let refNode;
if (referralCode) {
const ref = await this.prisma.user.findFirst({
where: {
Node: {
some: {
referral: referralCode,
},
},
},
});
refNode = await this.prisma.node.findFirst({
where: {
referral: referralCode,
},
});
referralUser = ref;
}
console.log(referralUser);
console.log(refNode);
const dd = await this.getUserByAddress(address);
if (dd) {
return dd;
}
const user = await this.prisma.user.create({
data: {
Node: {
connectOrCreate: {
where: {
address,
},
create: {
depth: !!refNode ? refNode.depth + 1 : 0,
address,
referral: generateRandomString(7),
parent: referralCode
? {
connect: {
referral: referralCode,
},
}
: undefined,
},
},
},
nickname: nickname ?? 'unknown',
IMEI,
mac,
pushAllowAt,
// referralUser : referralCode ? {
// connect : {
// userId :referralUser.userId,
// }
// }: undefined,
},
include: {
Node: {
include: {
parent: true,
children: true,
},
},
},
});
return user;
}
async getUserByAddress(address: string): Promise<User> {
return await this.prisma.user.findFirst({
where: {
Node: {
some: {
address: address,
},
},
},
include: {
Node: {
include: {
parent: true,
children: true,
},
},
},
});
}
async updateUser(userId: string, data: UpdateUserReq): Promise<any> {
return await this.prisma.user.update({
where: {
userId: userId,
},
data,
});
}
async getReffalUser(userId: string): Promise<User> {
var nodes = await this.prisma.node.findMany({
where: {
userId,
},
select: {
id: true,
},
});
var refferal = await this.prisma.node.findMany({
where: {
parent: {
parentId: {
in: nodes.map((node) => {
return node.id;
}),
},
},
},
select: {
address: true,
},
});
return refferal;
}
async getUsernotification(
userId: string,
prop: {
skip: number;
take: number;
},
): Promise<userNotification[]> {
const { skip, take } = prop;
return await this.prisma.userNotification.findMany({
where: {
userId,
},
skip,
take,
});
}
}

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

@@ -0,0 +1,7 @@
import { User } from './user';
describe('User', () => {
it('should be defined', () => {
expect(new User()).toBeDefined();
});
});

1
src/user/user.ts Normal file
View File

@@ -0,0 +1 @@
export class User {}

10
src/utils/randomstring.ts Normal file
View File

@@ -0,0 +1,10 @@
export function generateRandomString(length) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
result += characters[randomIndex];
}
return result;
}

13
src/utils/response.ts Normal file
View File

@@ -0,0 +1,13 @@
export type Response = {
result: boolean;
code: number;
data?: Payload | Payload[];
message?: string;
count?: number;
offset?: number;
limit?: number;
};
export type Payload = {
[key: string]: any;
};

View File

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

View File

@@ -0,0 +1,88 @@
import { Controller, Get, Post, Param, Body } from '@nestjs/common';
import {
ApiCreatedResponse,
ApiOperation,
ApiTags,
ApiBody,
ApiProperty,
} from '@nestjs/swagger';
class Transaction {
@ApiProperty()
form: string;
@ApiProperty()
to: string;
@ApiProperty()
fee: string;
@ApiProperty()
value: string;
@ApiProperty()
txid: string;
@ApiProperty()
type: string;
@ApiProperty()
internalFee: string;
@ApiProperty()
excutedFunction: string;
}
@Controller('wallet')
export class WalletController {
@Get('/history/')
@ApiOperation({ summary: '', description: '' })
getWalletHistory(@Param('address') address: string): any {
return {
result: true,
data: [
{
from: '0x1111111',
to: '0x2222222',
fee: '0.0',
value: '100.00',
txid: '0x0123456789abcdef',
type: 'in',
internalFee: '0.01',
excutedFunction: 'transfer',
},
{
from: '0x1111111',
to: '0x2222222',
fee: '0.0',
value: '100.00',
txid: '0x0123456789abcdef',
type: 'in',
internalFee: '0.01',
excutedFunction: 'transfer',
},
{
from: '0x1111111',
to: '0x2222222',
fee: '0.0',
value: '100.00',
txid: '0x0123456789abcdef',
type: 'in',
internalFee: '0.01',
excutedFunction: 'transfer',
},
],
};
}
@Get('/history/:txid')
@ApiOperation({ summary: '', description: '' })
getWalletHistoryTxid(@Param('txid') txid: string): any {
return {
result: true,
data: {
from: '0x1111111',
to: '0x2222222',
fee: '0.0',
value: '100.00',
txid: '0x0123456789abcdef',
type: 'in',
internalFee: '0.01',
excutedFunction: 'transfer',
},
};
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { WalletService } from './wallet.service';
import { WalletController } from './wallet.controller';
@Module({
providers: [WalletService],
controllers: [WalletController],
})
export class WalletModule {}

View File

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

View File

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