diff --git a/package-lock.json b/package-lock.json index 4521967cf..b11f2240e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6389,6 +6389,14 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -7413,6 +7421,11 @@ "node": ">=0.10.0" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "node_modules/is-ssh": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", @@ -9435,6 +9448,11 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -10070,6 +10088,51 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/mysql2": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.1.tgz", + "integrity": "sha512-O7FXjLtNkjcMBpLURwkXIhyVbX9i4lq4nNRCykPNOXfceq94kJ0miagmTEGCZieuO8JtwtXaZ41U6KT4eF9y3g==", + "dependencies": { + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru-cache": "^8.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mysql2/node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "engines": { + "node": ">=16.14" + } + }, + "node_modules/mysql2/node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -10081,6 +10144,25 @@ "thenify-all": "^1.0.0" } }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, "node_modules/nan": { "version": "2.17.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", @@ -12691,6 +12773,11 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -15052,8 +15139,12 @@ "@ckb-js/kuai-core": "0.0.1-alpha.2", "@ckb-js/kuai-io": "0.0.1-alpha.2", "@ckb-js/kuai-models": "0.0.1-alpha.2", + "@ckb-lumos/lumos": "0.20.0", + "http-errors": "2.0.0", "koa": "2.14.1", - "koa-body": "6.0.1" + "koa-body": "6.0.1", + "mysql2": "3.6.1", + "typeorm": "0.3.17" }, "devDependencies": { "ts-node": "10.9.1", diff --git a/packages/samples/sudt/package.json b/packages/samples/sudt/package.json index 5e7736d0a..726fe587e 100644 --- a/packages/samples/sudt/package.json +++ b/packages/samples/sudt/package.json @@ -6,7 +6,8 @@ "build:watch": "tsc -w", "start:prod": "node ./dist/main.js", "test": "jest", - "doc": "typedoc" + "doc": "typedoc", + "typeorm": "typeorm-ts-node-commonjs" }, "dependencies": { "@ckb-js/kuai-core": "0.0.1-alpha.2", @@ -15,7 +16,9 @@ "@ckb-lumos/lumos": "0.20.0", "http-errors": "2.0.0", "koa": "2.14.1", - "koa-body": "6.0.1" + "koa-body": "6.0.1", + "mysql2": "3.6.1", + "typeorm": "0.3.17" }, "devDependencies": { "ts-node": "10.9.1", diff --git a/packages/samples/sudt/src/controllers/sudt.controller.ts b/packages/samples/sudt/src/controllers/sudt.controller.ts index ecb21a414..cb23e6790 100644 --- a/packages/samples/sudt/src/controllers/sudt.controller.ts +++ b/packages/samples/sudt/src/controllers/sudt.controller.ts @@ -1,14 +1,24 @@ import type { HexString, Hash } from '@ckb-lumos/base' -import { BaseController, Controller, Body, Post } from '@ckb-js/kuai-io' import { ActorReference } from '@ckb-js/kuai-models' -import { BadRequest } from 'http-errors' +import { BadRequest, NotFound } from 'http-errors' import { SudtModel, appRegistry } from '../actors' import { Tx } from '../views/tx.view' import { getLock } from '../utils' -import { SudtResponse } from '../response' +import { BaseController, Body, Controller, Get, Param, Post, Put } from '@ckb-js/kuai-io' +import { SudtResponse } from '../../response' +import { CreateTokenRequest } from '../dto/create-token.dto' +import { DataSource, QueryFailedError } from 'typeorm' +import { Token } from '../entities/token.entity' +import { Account } from '../entities/account.entity' +import { tokenEntityToDto } from '../dto/token.dto' @Controller('sudt') export default class SudtController extends BaseController { + #explorerHost = process.env.EXPLORER_HOST || 'https://explorer.nervos.org' + constructor(private _dataSource: DataSource) { + super() + } + @Post('/send') async send( @Body() { from, to, amount, typeArgs }: { from: string[]; to: string; amount: HexString; typeArgs: Hash }, @@ -39,4 +49,58 @@ export default class SudtController extends BaseController { ) return SudtResponse.ok(await Tx.toJsonString(result)) } + + @Post('/token') + async createToken(@Body() req: CreateTokenRequest) { + let owner = await this._dataSource.getRepository(Account).findOneBy({ address: req.account }) + if (!owner) { + owner = await this._dataSource + .getRepository(Account) + .save(this._dataSource.getRepository(Account).create({ address: req.account })) + } + + try { + const token = await this._dataSource + .getRepository(Token) + .save(this._dataSource.getRepository(Token).create({ ...req, ownerId: owner.id })) + return new SudtResponse('201', { url: `${this.#explorerHost}/transaction/${token.typeId}` }) + } catch (e) { + if (e instanceof QueryFailedError) { + switch (e.driverError.code) { + case 'ER_DUP_ENTRY': + return SudtResponse.err('409', { message: 'Token already exists' }) + } + } + + console.error(e) + } + } + + @Put('/token') + async updateToken(@Body() req: CreateTokenRequest) { + const token = await this._dataSource.getRepository(Token).findOneBy({ typeId: req.typeId }) + if (token) { + await this._dataSource.getRepository(Token).save({ ...token, ...req }) + } + + return new SudtResponse('201', {}) + } + + @Get('/token/:typeId') + async getToken(@Param('typeId') typeId: string) { + const token = await this._dataSource.getRepository(Token).findOneBy({ typeId }) + + if (token) { + return SudtResponse.ok(tokenEntityToDto(token, '0', this.#explorerHost)) + } else { + throw new NotFound() + } + } + + @Get('/tokens') + async listTokens() { + const tokens = await this._dataSource.getRepository(Token).find() + + return SudtResponse.ok(tokens.map((token) => tokenEntityToDto(token, '0', this.#explorerHost))) + } } diff --git a/packages/samples/sudt/src/dto/token.dto.ts b/packages/samples/sudt/src/dto/token.dto.ts new file mode 100644 index 000000000..0c00ad2e6 --- /dev/null +++ b/packages/samples/sudt/src/dto/token.dto.ts @@ -0,0 +1,25 @@ +import { Token } from '../entities/token.entity' + +export interface TokenResponse { + symbol: string + name: string + amount: string + decimal: number + description: string + website: string + icon: string + explorerUrl: string +} + +export const tokenEntityToDto = (token: Token, amount: string, explorerHost: string): TokenResponse => { + return { + symbol: token.name, + name: token.name, + amount, + decimal: token.decimal, + description: token.description ?? '', + website: token.website, + icon: token.icon, + explorerUrl: `${explorerHost}/sudt/${token.typeId}`, + } +} diff --git a/packages/samples/sudt/src/entities/token.entity.ts b/packages/samples/sudt/src/entities/token.entity.ts new file mode 100644 index 000000000..35b8e2900 --- /dev/null +++ b/packages/samples/sudt/src/entities/token.entity.ts @@ -0,0 +1,43 @@ +import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn, Unique, UpdateDateColumn } from 'typeorm' + +@Entity() +export class Token { + @PrimaryGeneratedColumn() + id!: number + + @Column() + name!: string + + @Column({ default: 18 }) + decimal!: number + + @Column() + description?: string + + @Column({ default: '' }) + website!: string + + @Column({ default: '' }) + icon!: string + + @Column({ default: '' }) + txHash?: string + + @Column() + @Index() + ownerId!: number + + @Column() + @Unique('uniq_type_id', ['typeId']) + typeId!: string + + @Column() + @Unique('uniq_args', ['args']) + args!: string + + @CreateDateColumn() + createdAt!: Date + + @UpdateDateColumn() + updatedAt!: Date +} diff --git a/packages/samples/sudt/src/main.ts b/packages/samples/sudt/src/main.ts index 7bcba32f2..fd9cffc77 100644 --- a/packages/samples/sudt/src/main.ts +++ b/packages/samples/sudt/src/main.ts @@ -1,33 +1,52 @@ -import Koa from 'koa'; -import { koaBody } from 'koa-body'; -import { getGenesisScriptsConfig, initialKuai } from '@ckb-js/kuai-core'; -import { KoaRouterAdapter, CoR } from '@ckb-js/kuai-io'; -import OmnilockController from './controllers/omnilock.controller'; -import SudtController from './controllers/sudt.controller'; +import Koa from 'koa' +import { koaBody } from 'koa-body' +import { getGenesisScriptsConfig, initialKuai } from '@ckb-js/kuai-core' +import { KoaRouterAdapter, CoR } from '@ckb-js/kuai-io' +import SudtController from './controllers/sudt.controller' import { REDIS_HOST_SYMBOL, REDIS_OPT_SYMBOL, REDIS_PORT_SYMBOL, initiateResourceBindingManager, mqContainer, -} from '@ckb-js/kuai-models'; -import { config } from '@ckb-lumos/lumos'; -import './type-extends'; +} from '@ckb-js/kuai-models' +import { config } from '@ckb-lumos/lumos' +import './type-extends' +import 'dotenv/config' +import { DataSource } from 'typeorm' + +const initiateDataSource = async () => { + const dataSource = new DataSource({ + connectorPackage: 'mysql2', + type: 'mysql', + host: process.env.DB_HOST || 'localhost', + port: Number(process.env.DB_PORT) || 3306, + username: process.env.DB_USERNAME || 'root', + password: process.env.DB_PASSWORD || 'root', + database: process.env.DB_DATABASE || 'sudt', + entities: [__dirname + '/entities/*.{js,ts}'], + synchronize: true, + }) + + await dataSource.initialize() + + return dataSource +} async function bootstrap() { - const kuaiCtx = await initialKuai(); - const kuaiEnv = kuaiCtx.getRuntimeEnvironment(); + const kuaiCtx = await initialKuai() + const kuaiEnv = kuaiCtx.getRuntimeEnvironment() if (kuaiEnv.config.redisPort) { - mqContainer.bind(REDIS_PORT_SYMBOL).toConstantValue(kuaiEnv.config.redisPort); + mqContainer.bind(REDIS_PORT_SYMBOL).toConstantValue(kuaiEnv.config.redisPort) } if (kuaiEnv.config.redisHost) { - mqContainer.bind(REDIS_HOST_SYMBOL).toConstantValue(kuaiEnv.config.redisHost); + mqContainer.bind(REDIS_HOST_SYMBOL).toConstantValue(kuaiEnv.config.redisHost) } if (kuaiEnv.config.redisOpt) { - mqContainer.bind(REDIS_OPT_SYMBOL).toConstantValue(kuaiEnv.config.redisOpt); + mqContainer.bind(REDIS_OPT_SYMBOL).toConstantValue(kuaiEnv.config.redisOpt) } config.initializeConfig( @@ -37,42 +56,42 @@ async function bootstrap() { ...(await getGenesisScriptsConfig(kuaiEnv.config.ckbChain.rpcUrl)), }, }), - ); + ) + + const port = kuaiEnv.config?.port || 3000 - const port = kuaiEnv.config?.port || 3000; + initiateResourceBindingManager({ rpc: kuaiEnv.config.ckbChain.rpcUrl }) - initiateResourceBindingManager({ rpc: kuaiEnv.config.ckbChain.rpcUrl }); + const app = new Koa() + app.use(koaBody()) - const app = new Koa(); - app.use(koaBody()); + const dataSource = await initiateDataSource() // init kuai io - const cor = new CoR(); - const omnilockController = new OmnilockController(); - const sudtController = new SudtController(); - cor.use(omnilockController.middleware()); - cor.use(sudtController.middleware()); + const cor = new CoR() + const sudtController = new SudtController(dataSource) + cor.use(sudtController.middleware()) - const koaRouterAdapter = new KoaRouterAdapter(cor); + const koaRouterAdapter = new KoaRouterAdapter(cor) - app.use(koaRouterAdapter.routes()); + app.use(koaRouterAdapter.routes()) const server = app.listen(port, function () { const address = (() => { - const _address = server.address(); + const _address = server.address() if (!_address) { - return ''; + return '' } if (typeof _address === 'string') { - return _address; + return _address } - return `http://${_address.address}:${_address.port}`; - })(); + return `http://${_address.address}:${_address.port}` + })() - console.log(`kuai app listening at ${address}`); - }); + console.log(`kuai app listening at ${address}`) + }) } -bootstrap(); +bootstrap()