From b2ec018e5603c29a5c18138399ea60d28d371f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=83=E5=81=B6=E4=BB=80=E4=B9=88=E7=9A=84=E5=B0=B1?= =?UTF-8?q?=E6=98=AF=E5=B8=83=E5=81=B6?= Date: Wed, 15 Nov 2023 13:41:33 +0800 Subject: [PATCH 01/49] fix: create new capsule project when not found (#489) * fix: create new capsule project when not found * fix: not install capsule error --- .github/workflows/test.yaml | 6 ++++++ .../kuai-config-case/kuai.config.ts | 2 +- .../not-init-capsule-case/kuai.config.ts | 5 +++++ packages/cli/__tests__/index.ts | 13 ++++++++++++ packages/core/src/builtin-tasks/contract.ts | 20 +++++++++++++------ packages/core/src/builtin-tasks/signer.ts | 3 +-- 6 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 packages/cli/__tests__/__fixtures__/not-init-capsule-case/kuai.config.ts diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8c4de7ee..7c46800d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,6 +19,12 @@ jobs: - name: Install project dependencies and build run: npm i + - name: Install capsule + uses: actions-rs/install@v0.1 + with: + crate: ckb-capsule + version: latest + - name: Build packages run: npm run build diff --git a/packages/cli/__tests__/__fixtures__/kuai-config-case/kuai.config.ts b/packages/cli/__tests__/__fixtures__/kuai-config-case/kuai.config.ts index 620ca16a..0ad1bf55 100644 --- a/packages/cli/__tests__/__fixtures__/kuai-config-case/kuai.config.ts +++ b/packages/cli/__tests__/__fixtures__/kuai-config-case/kuai.config.ts @@ -34,6 +34,6 @@ subtask('demo-task2:sub').setAction(() => { console.info('subtask') }) -const config: KuaiConfig = {} +const config: KuaiConfig = { ckbChain: { prefix: '', rpcUrl: '' } } export default config diff --git a/packages/cli/__tests__/__fixtures__/not-init-capsule-case/kuai.config.ts b/packages/cli/__tests__/__fixtures__/not-init-capsule-case/kuai.config.ts new file mode 100644 index 00000000..3513f2fa --- /dev/null +++ b/packages/cli/__tests__/__fixtures__/not-init-capsule-case/kuai.config.ts @@ -0,0 +1,5 @@ +import { KuaiConfig } from '@ckb-js/kuai-core' + +const config: KuaiConfig = { ckbChain: { prefix: '', rpcUrl: '' } } + +export default config diff --git a/packages/cli/__tests__/index.ts b/packages/cli/__tests__/index.ts index bbefb52c..23938503 100644 --- a/packages/cli/__tests__/index.ts +++ b/packages/cli/__tests__/index.ts @@ -1,5 +1,6 @@ import { describe, expect, test, beforeAll, afterAll } from '@jest/globals' import { execSync, exec } from 'node:child_process' +import { rmSync } from 'node:fs' /* eslint-disable-next-line @typescript-eslint/no-var-requires */ const { scheduler } = require('node:timers/promises') const CONFIG_PATH = './__tests__/__fixtures__/kuai-config-case/kuai.config.ts' @@ -292,4 +293,16 @@ describe('kuai cli', () => { expect(output.toString().replace(ANSI_COLORS_CODE_REG, '')).toMatch('{ variadicParam: [ 1, 2 ] }') }) }) + + describe('contract command', () => { + test('not init capsule case', () => { + const output = execSync( + 'npx kuai contract new --name hello --config ./__tests__/__fixtures__/not-init-capsule-case/kuai.config.ts', + ) + expect(output.toString()).toMatch(/Done/) + + // remove temp files + rmSync('./__tests__/__fixtures__/not-init-capsule-case/contract', { recursive: true }) + }) + }) }) diff --git a/packages/core/src/builtin-tasks/contract.ts b/packages/core/src/builtin-tasks/contract.ts index 0a28ef14..40027dc0 100644 --- a/packages/core/src/builtin-tasks/contract.ts +++ b/packages/core/src/builtin-tasks/contract.ts @@ -496,16 +496,24 @@ subtask('contract:sign-message') }) subtask('contract:get-workspace').setAction(async (_, { config }) => { - if (config.contract?.workspace) { - return config.contract?.workspace - } - - const userConfigPath = getUserConfigPath() + const userConfigPath = config.kuaiArguments?.configPath || getUserConfigPath() if (!userConfigPath) { throw new Error('Please run in kuai project') } - return path.join(path.dirname(userConfigPath), 'contract') + const workspacePath = path.join(path.dirname(userConfigPath), config.contract?.workspace ?? 'contract') + + if (!existsSync(workspacePath)) { + const workspaceName = path.basename(workspacePath) + const workspaceDir = path.dirname(workspacePath) + + mkdirSync(workspaceDir, { recursive: true }) + execSync(`cd ${workspaceDir} && capsule new ${workspaceName}`, { + stdio: 'inherit', + }) + } + + return workspacePath }) subtask('contract:set-environment').setAction(async () => { diff --git a/packages/core/src/builtin-tasks/signer.ts b/packages/core/src/builtin-tasks/signer.ts index 05e1ccd6..bde6d4f6 100644 --- a/packages/core/src/builtin-tasks/signer.ts +++ b/packages/core/src/builtin-tasks/signer.ts @@ -21,8 +21,7 @@ subtask('signer:get-signer', 'get built-in signer').setAction(async (_, env) => } const ckbcli = new CKBCLI({ url: env.config.ckbChain.rpcUrl }) - - const userConfigPath = getUserConfigPath() + const userConfigPath = env.config.kuaiArguments?.configPath || getUserConfigPath() if (!userConfigPath) { throw new Error('Please run in kuai project') } From 4c9a449da4983267239670705df9bc9cf801a09e Mon Sep 17 00:00:00 2001 From: daryl Date: Sat, 16 Sep 2023 10:57:15 +0800 Subject: [PATCH 02/49] feat(sudt-demo): add api doc --- packages/samples/sudt/README.md | 186 ++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/packages/samples/sudt/README.md b/packages/samples/sudt/README.md index 0242eaa0..58969a2a 100644 --- a/packages/samples/sudt/README.md +++ b/packages/samples/sudt/README.md @@ -9,3 +9,189 @@ npm run build node ./dist/src/main.js ``` + +## API Doc + +### Create Token + +path: /token + +method: POST + +#### Request + +```javascript +{ + "symbol": "USDT", + "name": "USDT", + "amount": "100000", + "decimal": "18", + "description": "", + "website": "", + "icon": "" +} +``` + +#### Reponse + +```javascript +{ + "code": 201, + "data": { + "url": "" // direct to explorer to the transaction to issue the token + } +} +``` + +### Update Token + +path: /token + +method: PUT + +#### Request + +```javascript +{ + "symbol": "USDT", + "name": "USDT", + "amount": "100000", + "decimal": "18", + "description": "", + "website": "", + "icon": "", + "args": "", // sudt args + "signature": "" +} +``` + +#### Response + +```javascript +{ + "code": 201, + "data": {} +} +``` + +### Transfer Token + +path: /token/transfer + +method: POST + +#### Request + +```javascript +{ + "token": "", // token args + "amount": "", + "to": "" +} +``` + +### Token List + +path: /token + +#### Request + +| param | type | position | description | +| ------- | ------ | -------- | ------------ | +| address | string | query | user address | + +method: GET + +#### Response + +```javascript +[ + { + symbol: 'USDT', + name: 'USDT', + amount: '100000', + decimal: '18', + description: '', + website: '', + icon: '', + }, +]; +``` + +### Token Detail + +path: /token/:args + +method: GET + +#### Response + +```javascript +{ + "code": 200, + "data": { + "symbol": "USDT", + "name": "USDT", + "amount": "100000", + "decimal": "18", + "description": "", + "website": "", + "icon": "", + "url": "", + "issuser": "" + } +} +``` + +### Asset List + +path: /assets + +method: GET + +#### Request + +| param | type | position | description | +| ------- | ------ | -------- | ------------ | +| address | string | query | user address | + +#### Response + +```javascript +{ + "code": 200, + "data": [ + { + "symbol": "USDT", + "name": "USDT", + "amount": "" + } + ] +} +``` + +### Token Transfer History + +path: /token/transfer/history + +method: GET + +#### Response + +```javascript +{ + "code": 200, + "data": [ + { + "txHash": "", + "from": "", + "to": "", + "time": "", + "status": "", + "sudtAmount": "", + "CKBAmount": "", + "url": "", + } + ] +} +``` From 1a398ed5ac463cb06df5deebfacadc8f6d475f84 Mon Sep 17 00:00:00 2001 From: daryl Date: Thu, 12 Oct 2023 23:00:47 +0800 Subject: [PATCH 03/49] fix: emit decorator metadata in ts config --- packages/samples/sudt/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/samples/sudt/tsconfig.json b/packages/samples/sudt/tsconfig.json index 82906436..5c0324da 100644 --- a/packages/samples/sudt/tsconfig.json +++ b/packages/samples/sudt/tsconfig.json @@ -7,7 +7,8 @@ "forceConsistentCasingInFileNames": true, "experimentalDecorators": true, "strict": true, - "skipLibCheck": true + "skipLibCheck": true, + "emitDecoratorMetadata": true }, "ts-node": { "files": true From 3a03a40210595086a4ab7acc1bc6bacbd0a59308 Mon Sep 17 00:00:00 2001 From: daryl Date: Thu, 12 Oct 2023 23:04:43 +0800 Subject: [PATCH 04/49] feat(sudt-manager): add response formatter --- packages/samples/sudt/response.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 packages/samples/sudt/response.ts diff --git a/packages/samples/sudt/response.ts b/packages/samples/sudt/response.ts new file mode 100644 index 00000000..b461f0af --- /dev/null +++ b/packages/samples/sudt/response.ts @@ -0,0 +1,14 @@ +export class SudtResponse { + constructor( + private code: string, + private data: T, + ) {} + + static ok(data: T) { + return new SudtResponse('200', data) + } + + static err(code: string, data: T) { + return new SudtResponse(code, data) + } +} From f0cfc40e26965e0db2556731b715b2517bfb1bd5 Mon Sep 17 00:00:00 2001 From: daryl Date: Thu, 12 Oct 2023 23:59:55 +0800 Subject: [PATCH 05/49] feat(sudt): sudt create, update and detail --- package-lock.json | 91 ++++++++++++++++++- packages/samples/sudt/README.md | 80 +++++++++------- packages/samples/sudt/package.json | 7 +- .../sudt/src/controllers/sudt.controller.ts | 70 +++++++++++++- .../samples/sudt/src/dto/create-token.dto.ts | 17 ++++ packages/samples/sudt/src/dto/token.dto.ts | 25 +++++ .../sudt/src/entities/account.entity.ts | 16 ++++ .../samples/sudt/src/entities/token.entity.ts | 43 +++++++++ packages/samples/sudt/src/main.ts | 87 +++++++++++------- 9 files changed, 364 insertions(+), 72 deletions(-) create mode 100644 packages/samples/sudt/src/dto/create-token.dto.ts create mode 100644 packages/samples/sudt/src/dto/token.dto.ts create mode 100644 packages/samples/sudt/src/entities/account.entity.ts create mode 100644 packages/samples/sudt/src/entities/token.entity.ts diff --git a/package-lock.json b/package-lock.json index 457ed6c0..a1ca7452 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6393,6 +6393,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", @@ -7416,6 +7424,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", @@ -9454,6 +9467,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", @@ -10089,6 +10107,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", @@ -10100,6 +10163,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", @@ -12710,6 +12792,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", @@ -15074,7 +15161,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/README.md b/packages/samples/sudt/README.md index 58969a2a..b1bd8530 100644 --- a/packages/samples/sudt/README.md +++ b/packages/samples/sudt/README.md @@ -22,13 +22,20 @@ method: POST ```javascript { - "symbol": "USDT", - "name": "USDT", - "amount": "100000", - "decimal": "18", - "description": "", - "website": "", - "icon": "" + "code": 200, + "data": { + "symbol": "USDT", + "name": "USDT", + "account": "", // the args of account in omnilock + "supply": "100000", + "decimal": 18, + "description": "", + "website": "", + "icon": "", + "typeId": "", + "args": "", // the args of sudt type script + "explorerCode": "" // the verify code from explorer + } } ``` @@ -53,15 +60,18 @@ method: PUT ```javascript { - "symbol": "USDT", - "name": "USDT", - "amount": "100000", - "decimal": "18", - "description": "", - "website": "", - "icon": "", - "args": "", // sudt args - "signature": "" + "code": 200, + "data": { + "symbol": "USDT", + "name": "USDT", + "amount": "100000", + "decimal": "18", + "description": "", + "website": "", + "icon": "", + "args": "", // sudt args + "signature": "" + } } ``` @@ -84,9 +94,12 @@ method: POST ```javascript { - "token": "", // token args - "amount": "", - "to": "" + "code": 200, + "data": { + "token": "", // token args + "amount": "", + "to": "" + } } ``` @@ -105,17 +118,20 @@ method: GET #### Response ```javascript -[ - { - symbol: 'USDT', - name: 'USDT', - amount: '100000', - decimal: '18', - description: '', - website: '', - icon: '', - }, -]; +{ + "code": 200, + "data": [ + { + symbol: 'USDT', + name: 'USDT', + amount: '100000', + decimal: '18', + description: '', + website: '', + icon: '', + }, + ] +} ``` ### Token Detail @@ -145,7 +161,7 @@ method: GET ### Asset List -path: /assets +path: /account/:address/assets method: GET @@ -172,7 +188,7 @@ method: GET ### Token Transfer History -path: /token/transfer/history +path: /account/:address/assets/transfer/history method: GET diff --git a/packages/samples/sudt/package.json b/packages/samples/sudt/package.json index 2e1b042f..49b64236 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 408c0d18..dc6de30c 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, Get, Param } 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() + } + @Get('/meta/:typeArgs') async meta(@Param('typeArgs') typeArgs: string) { if (!typeArgs) { @@ -61,4 +71,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/create-token.dto.ts b/packages/samples/sudt/src/dto/create-token.dto.ts new file mode 100644 index 00000000..84156617 --- /dev/null +++ b/packages/samples/sudt/src/dto/create-token.dto.ts @@ -0,0 +1,17 @@ +export interface CreateTokenRequest { + name: string + symbol: string + supply: string + account: string + decimal: number + description: string + website: string + icon: string + typeId: string + explorerCode: string + args: string +} + +export interface CreateTokenResponse { + url: string +} 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 00000000..0c00ad2e --- /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/account.entity.ts b/packages/samples/sudt/src/entities/account.entity.ts new file mode 100644 index 00000000..0d9e5935 --- /dev/null +++ b/packages/samples/sudt/src/entities/account.entity.ts @@ -0,0 +1,16 @@ +import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm' + +@Entity() +export class Account { + @PrimaryGeneratedColumn() + id!: number + + @Column({ type: 'text' }) + address!: string + + @CreateDateColumn() + createdAt!: Date + + @UpdateDateColumn() + updatedAt!: Date +} 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 00000000..35b8e290 --- /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 7bcba32f..fd9cffc7 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() From afe3ce01d5fb937c9e09dfa9a01d667615b3062c Mon Sep 17 00:00:00 2001 From: daryl Date: Fri, 13 Oct 2023 12:40:00 +0800 Subject: [PATCH 06/49] feat(sudt): add account controller --- packages/common/src/util.ts | 19 +++- .../samples/sudt/src/actors/sudt.model.ts | 9 +- packages/samples/sudt/src/constant.ts | 8 ++ .../src/controllers/account.controller.ts | 92 +++++++++++++++++++ .../sudt/src/dto/pre-create-token.dto.ts | 12 +++ packages/samples/sudt/src/dto/transfer.dto.ts | 12 +++ packages/samples/sudt/src/main.ts | 3 + packages/samples/sudt/src/views/tx.view.ts | 61 +++--------- 8 files changed, 163 insertions(+), 53 deletions(-) create mode 100644 packages/samples/sudt/src/constant.ts create mode 100644 packages/samples/sudt/src/controllers/account.controller.ts create mode 100644 packages/samples/sudt/src/dto/pre-create-token.dto.ts create mode 100644 packages/samples/sudt/src/dto/transfer.dto.ts diff --git a/packages/common/src/util.ts b/packages/common/src/util.ts index b608607f..8ecfc6de 100644 --- a/packages/common/src/util.ts +++ b/packages/common/src/util.ts @@ -1,5 +1,5 @@ import type { TransactionWithStatus } from '@ckb-lumos/base' -import type { RPC } from '@ckb-lumos/lumos' +import { config, type RPC, type helpers } from '@ckb-lumos/lumos' import { scheduler } from 'node:timers/promises' import path from 'node:path' import fs from 'node:fs' @@ -84,3 +84,20 @@ export async function getPackageJson(): Promise { const root = getPackageRoot() return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf-8')) } + +export const addBuiltInCellDeps = (txSkeleton: helpers.TransactionSkeletonType, dep: string) => { + const depConfig = config.getConfig().SCRIPTS[dep] + if (depConfig) { + txSkeleton = txSkeleton.update('cellDeps', (cellDeps) => + cellDeps.push({ + outPoint: { + txHash: depConfig.TX_HASH, + index: depConfig.INDEX, + }, + depType: depConfig.DEP_TYPE, + }), + ) + } + + return txSkeleton +} diff --git a/packages/samples/sudt/src/actors/sudt.model.ts b/packages/samples/sudt/src/actors/sudt.model.ts index 1b9dc81f..352da589 100644 --- a/packages/samples/sudt/src/actors/sudt.model.ts +++ b/packages/samples/sudt/src/actors/sudt.model.ts @@ -15,6 +15,8 @@ import { UpdateStorageValue, TypeFilter, Sudt, + LockFilter, + Omnilock, } from '@ckb-js/kuai-models' import type { Cell, HexString, Script } from '@ckb-lumos/base' import { number, bytes } from '@ckb-lumos/codec' @@ -27,10 +29,13 @@ import { MIN_SUDT_WITH_OMINILOCK, TX_FEE } from '../const' */ @ActorProvider({ ref: { name: 'sudt', path: `/:args/` } }) @TypeFilter() +@LockFilter() +@Omnilock() @Sudt() export class SudtModel extends JSONStore> { constructor( - @Param('args') args: string, + @Param('typeArgs') typeArgs: string, + @Param('lockArgs') lockArgs: string, _schemaOption?: void, params?: { states?: Record @@ -39,7 +44,7 @@ export class SudtModel extends JSONStore> { schemaPattern?: SchemaPattern }, ) { - super(undefined, { ...params, ref: ActorReference.newWithFilter(SudtModel, `/${args}/`) }) + super(undefined, { ...params, ref: ActorReference.newWithFilter(SudtModel, `/${lockArgs}/${typeArgs}/`) }) if (!this.typeScript) { throw new Error('type script is required') } diff --git a/packages/samples/sudt/src/constant.ts b/packages/samples/sudt/src/constant.ts new file mode 100644 index 00000000..6a6f44ae --- /dev/null +++ b/packages/samples/sudt/src/constant.ts @@ -0,0 +1,8 @@ +/** + * @module src/const + * @description + * This module defines the constants used in the application. + */ + +export const TX_FEE = 100000 +export const MIN_SUDT_WITH_OMINILOCK = 14400000000 diff --git a/packages/samples/sudt/src/controllers/account.controller.ts b/packages/samples/sudt/src/controllers/account.controller.ts new file mode 100644 index 00000000..e97c6656 --- /dev/null +++ b/packages/samples/sudt/src/controllers/account.controller.ts @@ -0,0 +1,92 @@ +import { BaseController, Body, Controller, Get, Param, Post } from '@ckb-js/kuai-io' +import { DataSource } from 'typeorm' +import { Transaction } from '../entities/transaction.entity' +import { Account } from '../entities/account.entity' +import { OmnilockModel, SudtModel, appRegistry } from '../actors' +import { ActorReference } from '@ckb-js/kuai-models' +import { getLock } from '../utils' +import { SudtResponse } from '../response' +import { Token } from '../entities/token.entity' +import { BadRequest } from 'http-errors' +import { HexString } from '@ckb-lumos/lumos' +import { Tx } from '../views/tx.view' + +@Controller('/account') +export class AccountController extends BaseController { + #explorerHost = process.env.EXPLORER_HOST || 'https://explorer.nervos.org' + constructor(private _dataSource: DataSource) { + super() + } + + @Post('/mint') + async mint(@Body() { from, to, amount }: { from: string; to: string; amount: HexString }) { + if (!from || !to || !amount) { + throw new BadRequest('undefined body field: from, to or amount') + } + + const omniLockModel = appRegistry.findOrBind( + new ActorReference('omnilock', `/${getLock(from).args}/`), + ) + + const result = omniLockModel.mint(getLock(to), amount) + + return SudtResponse.ok(await Tx.toJsonString(result)) + } + + @Get('/meta/:address') + async meta(@Param('address') address: string) { + if (!address) { + throw new Request('invalid address') + } + + const repo = this._dataSource.getRepository(Account) + const account = await repo.findBy({ address }) + if (!account) { + repo.save(repo.create({ address })) + } + + const omniLockModel = appRegistry.findOrBind( + new ActorReference('omnilock', `/${getLock(address).args}/`), + ) + + return SudtResponse.ok(omniLockModel?.meta) + } + + @Get('/:address/assets/transaction') + async accountTransaction(@Param('address') address: string) { + const account = await this._dataSource.getRepository(Account).findOneBy({ address }) + if (!account) { + return [] + } + + const txs = await this._dataSource.getRepository(Transaction).findBy({ fromAccountId: account.id }) + return txs.map((tx) => ({ + txHash: tx.txHash, + from: tx.fromAccountId, + to: tx.toAccountId, + time: tx.createdAt, + status: tx.status, + sudtAmount: tx.sudtAmount, + ckbAmount: tx.ckbAmount, + url: `${this.#explorerHost}/transaction/${tx.txHash}`, + })) + } + + @Get('/:address/assets') + async accountAssets(@Param('address') address: string) { + const tokens = await this._dataSource.getRepository(Token).find() + const lock = getLock(address) + return tokens.map((token) => { + try { + return { + ...token, + ...appRegistry + .findOrBind(new ActorReference('sudt', `/${lock.args}/${token.args}/`)) + .getSudtBalance([getLock(address)]), + } + } catch (e) { + console.error(e) + } + }) + } +} diff --git a/packages/samples/sudt/src/dto/pre-create-token.dto.ts b/packages/samples/sudt/src/dto/pre-create-token.dto.ts new file mode 100644 index 00000000..1823e3a7 --- /dev/null +++ b/packages/samples/sudt/src/dto/pre-create-token.dto.ts @@ -0,0 +1,12 @@ +import { HexString } from '@ckb-lumos/lumos' +import { TransactionSkeletonType } from '@ckb-lumos/helpers' + +export interface PreCreateTokenRequest { + from: string + to: string + amount: HexString +} + +export interface PreCreateTokenResponse { + txSkeleton: TransactionSkeletonType +} diff --git a/packages/samples/sudt/src/dto/transfer.dto.ts b/packages/samples/sudt/src/dto/transfer.dto.ts new file mode 100644 index 00000000..b975519a --- /dev/null +++ b/packages/samples/sudt/src/dto/transfer.dto.ts @@ -0,0 +1,12 @@ +import { TransactionSkeletonObject } from '@ckb-lumos/helpers' + +export interface TransferRequest { + from: string[] + to: string + amount: bigint + typeId: string +} + +export interface TransferResponse { + skeleton: TransactionSkeletonObject +} diff --git a/packages/samples/sudt/src/main.ts b/packages/samples/sudt/src/main.ts index fd9cffc7..fdb2e59f 100644 --- a/packages/samples/sudt/src/main.ts +++ b/packages/samples/sudt/src/main.ts @@ -14,6 +14,7 @@ import { config } from '@ckb-lumos/lumos' import './type-extends' import 'dotenv/config' import { DataSource } from 'typeorm' +import { AccountController } from './controllers/account.controller' const initiateDataSource = async () => { const dataSource = new DataSource({ @@ -70,7 +71,9 @@ async function bootstrap() { // init kuai io const cor = new CoR() const sudtController = new SudtController(dataSource) + const accountController = new AccountController(dataSource) cor.use(sudtController.middleware()) + cor.use(accountController.middleware()) const koaRouterAdapter = new KoaRouterAdapter(cor) diff --git a/packages/samples/sudt/src/views/tx.view.ts b/packages/samples/sudt/src/views/tx.view.ts index 32e444ce..fd026b29 100644 --- a/packages/samples/sudt/src/views/tx.view.ts +++ b/packages/samples/sudt/src/views/tx.view.ts @@ -1,7 +1,5 @@ -import { type Cell, helpers, config } from '@ckb-lumos/lumos' -import { SECP_SIGNATURE_PLACEHOLDER, OMNILOCK_SIGNATURE_PLACEHOLDER } from '@ckb-lumos/common-scripts/lib/helper' -import { blockchain } from '@ckb-lumos/base' -import { bytes } from '@ckb-lumos/codec' +import { type Cell, helpers, commons } from '@ckb-lumos/lumos' +import { addBuiltInCellDeps } from '@ckb-js/kuai-common' export class Tx { static async toJsonString({ @@ -14,55 +12,18 @@ export class Tx { witnesses?: string[] }): Promise { let txSkeleton = helpers.TransactionSkeleton({}) + for (const input of inputs) { + txSkeleton = await commons.omnilock.setupInputCell(txSkeleton, input) + txSkeleton = txSkeleton.remove('outputs') + } txSkeleton = txSkeleton.update('outputs', (v) => v.push(...outputs)) - const CONFIG = config.getConfig() - txSkeleton = txSkeleton.update('cellDeps', (v) => - v.push( - { - outPoint: { - txHash: CONFIG.SCRIPTS.SUDT!.TX_HASH, - index: CONFIG.SCRIPTS.SUDT!.INDEX, - }, - depType: CONFIG.SCRIPTS.SUDT!.DEP_TYPE, - }, - { - outPoint: { - txHash: CONFIG.SCRIPTS.OMNILOCK!.TX_HASH, - index: CONFIG.SCRIPTS.OMNILOCK!.INDEX, - }, - depType: CONFIG.SCRIPTS.OMNILOCK!.DEP_TYPE, - }, - { - outPoint: { - txHash: CONFIG.SCRIPTS.SECP256K1_BLAKE160!.TX_HASH, - index: CONFIG.SCRIPTS.SECP256K1_BLAKE160!.INDEX, - }, - depType: CONFIG.SCRIPTS.SECP256K1_BLAKE160!.DEP_TYPE, - }, - ), - ) + txSkeleton = addBuiltInCellDeps(txSkeleton, 'SUDT') - inputs.forEach((input, idx) => { - txSkeleton = txSkeleton.update('inputs', (inputs) => inputs.push(input)) - - txSkeleton = txSkeleton.update('witnesses', (wit) => { - if (!witnesses?.[idx] || witnesses?.[idx] === '0x' || witnesses?.[idx] === '') { - const omniLock = CONFIG.SCRIPTS.OMNILOCK as NonNullable - const fromLockScript = input.cellOutput.lock - return wit.push( - bytes.hexify( - blockchain.WitnessArgs.pack({ - lock: - omniLock.CODE_HASH === fromLockScript.codeHash && fromLockScript.hashType === omniLock.HASH_TYPE - ? OMNILOCK_SIGNATURE_PLACEHOLDER - : SECP_SIGNATURE_PLACEHOLDER, - }), - ), - ) - } - return wit.push(witnesses?.[idx]) + if (witnesses) { + witnesses.forEach((witness) => { + txSkeleton = txSkeleton.update('witnesses', (v) => v.push(witness)) }) - }) + } return helpers.transactionSkeletonToObject(txSkeleton) } From 127d25856cf6ed98f0302ddd477741c0bfff7996 Mon Sep 17 00:00:00 2001 From: daryl Date: Fri, 13 Oct 2023 13:21:02 +0800 Subject: [PATCH 07/49] feat(sudt): transaction entity for db --- .../sudt/src/entities/transaction.entity.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 packages/samples/sudt/src/entities/transaction.entity.ts diff --git a/packages/samples/sudt/src/entities/transaction.entity.ts b/packages/samples/sudt/src/entities/transaction.entity.ts new file mode 100644 index 00000000..20ffde76 --- /dev/null +++ b/packages/samples/sudt/src/entities/transaction.entity.ts @@ -0,0 +1,40 @@ +import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm' + +export enum TransactionStatus { + New = 1, + Pending, + Committed, +} + +@Entity() +export class Transaction { + @PrimaryGeneratedColumn() + id!: number + + @Column() + typeId!: string + + @Column() + txHash!: string + + @Column({ type: 'tinyint' }) + status!: TransactionStatus + + @Column() + fromAccountId!: number + + @Column() + toAccountId!: number + + @Column() + sudtAmount!: string + + @Column() + ckbAmount!: string + + @CreateDateColumn() + createdAt!: Date + + @UpdateDateColumn() + updatedAt!: Date +} From e12c58694a4968f556cb25cb6a84a9ff178350a4 Mon Sep 17 00:00:00 2001 From: daryl Date: Fri, 13 Oct 2023 16:06:45 +0800 Subject: [PATCH 08/49] style(sudt): extract bootstrap from the main --- packages/samples/sudt/src/bootstrap.ts | 96 +++++++++++++++++++++++++ packages/samples/sudt/src/main.ts | 97 +------------------------- 2 files changed, 97 insertions(+), 96 deletions(-) create mode 100644 packages/samples/sudt/src/bootstrap.ts diff --git a/packages/samples/sudt/src/bootstrap.ts b/packages/samples/sudt/src/bootstrap.ts new file mode 100644 index 00000000..91bf0eac --- /dev/null +++ b/packages/samples/sudt/src/bootstrap.ts @@ -0,0 +1,96 @@ +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 { DataSource } from 'typeorm' +import { AccountController } from './controllers/account.controller' + +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 +} + +export const bootstrap = async () => { + const kuaiCtx = await initialKuai() + const kuaiEnv = kuaiCtx.getRuntimeEnvironment() + + if (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) + } + + if (kuaiEnv.config.redisOpt) { + mqContainer.bind(REDIS_OPT_SYMBOL).toConstantValue(kuaiEnv.config.redisOpt) + } + + config.initializeConfig( + config.createConfig({ + PREFIX: kuaiEnv.config.ckbChain.prefix, + SCRIPTS: kuaiEnv.config.ckbChain.scripts || { + ...(await getGenesisScriptsConfig(kuaiEnv.config.ckbChain.rpcUrl)), + }, + }), + ) + + const port = kuaiEnv.config?.port || 3000 + + initiateResourceBindingManager({ rpc: kuaiEnv.config.ckbChain.rpcUrl }) + + const app = new Koa() + app.use(koaBody()) + + const dataSource = await initiateDataSource() + + // init kuai io + const cor = new CoR() + const sudtController = new SudtController(dataSource) + const accountController = new AccountController(dataSource) + cor.use(sudtController.middleware()) + cor.use(accountController.middleware()) + + const koaRouterAdapter = new KoaRouterAdapter(cor) + + app.use(koaRouterAdapter.routes()) + + const server = app.listen(port, function () { + const address = (() => { + const _address = server.address() + if (!_address) { + return '' + } + + if (typeof _address === 'string') { + return _address + } + + return `http://${_address.address}:${_address.port}` + })() + + console.log(`kuai app listening at ${address}`) + }) +} diff --git a/packages/samples/sudt/src/main.ts b/packages/samples/sudt/src/main.ts index fdb2e59f..2895401f 100644 --- a/packages/samples/sudt/src/main.ts +++ b/packages/samples/sudt/src/main.ts @@ -1,100 +1,5 @@ -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' import 'dotenv/config' -import { DataSource } from 'typeorm' -import { AccountController } from './controllers/account.controller' - -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() - - if (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) - } - - if (kuaiEnv.config.redisOpt) { - mqContainer.bind(REDIS_OPT_SYMBOL).toConstantValue(kuaiEnv.config.redisOpt) - } - - config.initializeConfig( - config.createConfig({ - PREFIX: kuaiEnv.config.ckbChain.prefix, - SCRIPTS: kuaiEnv.config.ckbChain.scripts || { - ...(await getGenesisScriptsConfig(kuaiEnv.config.ckbChain.rpcUrl)), - }, - }), - ) - - const port = kuaiEnv.config?.port || 3000 - - initiateResourceBindingManager({ rpc: kuaiEnv.config.ckbChain.rpcUrl }) - - const app = new Koa() - app.use(koaBody()) - - const dataSource = await initiateDataSource() - - // init kuai io - const cor = new CoR() - const sudtController = new SudtController(dataSource) - const accountController = new AccountController(dataSource) - cor.use(sudtController.middleware()) - cor.use(accountController.middleware()) - - const koaRouterAdapter = new KoaRouterAdapter(cor) - - app.use(koaRouterAdapter.routes()) - - const server = app.listen(port, function () { - const address = (() => { - const _address = server.address() - if (!_address) { - return '' - } - - if (typeof _address === 'string') { - return _address - } - - return `http://${_address.address}:${_address.port}` - })() - - console.log(`kuai app listening at ${address}`) - }) -} +import { bootstrap } from './bootstrap' bootstrap() From 057ca6cd6e65c1c74f49977526347d3b969a0696 Mon Sep 17 00:00:00 2001 From: daryl Date: Tue, 24 Oct 2023 11:58:15 +0800 Subject: [PATCH 09/49] feat(sudt): mint api --- .../samples/sudt/src/actors/omnilock.model.ts | 3 ++- .../sudt/src/controllers/account.controller.ts | 17 +++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/samples/sudt/src/actors/omnilock.model.ts b/packages/samples/sudt/src/actors/omnilock.model.ts index 1fc5dff7..41ad7076 100644 --- a/packages/samples/sudt/src/actors/omnilock.model.ts +++ b/packages/samples/sudt/src/actors/omnilock.model.ts @@ -61,6 +61,7 @@ export class OmnilockModel extends JSONStore> { mint( lockScript: Script, amount: HexString, + args?: string, ): { inputs: Cell[] outputs: Cell[] @@ -74,7 +75,7 @@ export class OmnilockModel extends JSONStore> { type: { codeHash: CONFIG.SCRIPTS.SUDT!.CODE_HASH, hashType: CONFIG.SCRIPTS.SUDT!.HASH_TYPE, - args: utils.computeScriptHash(this.lockScript!), + args: args ?? utils.computeScriptHash(lockScript), }, }, data: bytes.hexify(number.Uint128LE.pack(amount)), diff --git a/packages/samples/sudt/src/controllers/account.controller.ts b/packages/samples/sudt/src/controllers/account.controller.ts index e97c6656..e5d3ac10 100644 --- a/packages/samples/sudt/src/controllers/account.controller.ts +++ b/packages/samples/sudt/src/controllers/account.controller.ts @@ -8,8 +8,8 @@ import { getLock } from '../utils' import { SudtResponse } from '../response' import { Token } from '../entities/token.entity' import { BadRequest } from 'http-errors' -import { HexString } from '@ckb-lumos/lumos' import { Tx } from '../views/tx.view' +import { MintRequest } from '../dto/mint.dto' @Controller('/account') export class AccountController extends BaseController { @@ -18,17 +18,22 @@ export class AccountController extends BaseController { super() } - @Post('/mint') - async mint(@Body() { from, to, amount }: { from: string; to: string; amount: HexString }) { - if (!from || !to || !amount) { + @Post('/mint/:typeId') + async mint(@Body() { from, to, amount }: MintRequest, @Param('typeId') typeId: string) { + if (!from || from.length === 0 || !to || !amount) { throw new BadRequest('undefined body field: from, to or amount') } + const token = await this._dataSource.getRepository(Token).findOneBy({ typeId }) + if (!token) { + return SudtResponse.err(404, 'token not found') + } + const omniLockModel = appRegistry.findOrBind( - new ActorReference('omnilock', `/${getLock(from).args}/`), + new ActorReference('omnilock', `/${getLock(from[0]).args}/`), ) - const result = omniLockModel.mint(getLock(to), amount) + const result = omniLockModel.mint(getLock(to), amount, token.args) return SudtResponse.ok(await Tx.toJsonString(result)) } From 076e4c59e9a6ddd5192557c7f4299f6d5cddc872 Mon Sep 17 00:00:00 2001 From: daryl Date: Tue, 24 Oct 2023 12:04:49 +0800 Subject: [PATCH 10/49] feat(sudt): update doc --- packages/samples/sudt/README.md | 333 +++++++++++++++++++++++++++++--- 1 file changed, 303 insertions(+), 30 deletions(-) diff --git a/packages/samples/sudt/README.md b/packages/samples/sudt/README.md index b1bd8530..316f62f4 100644 --- a/packages/samples/sudt/README.md +++ b/packages/samples/sudt/README.md @@ -12,6 +12,113 @@ node ./dist/src/main.js ## API Doc +### Mint Token + +path: /sudt/mint/:typeId + +method: POST + +#### Request + +```javascript +{ + "from": [""], + "to": "", + "amount": "1000", +} +``` + +#### Response + +```javascript +{ + "code": 200, + "data": { + "txSkeleton": "txSkeleton": { + "cellProvider": null, + "cellDeps": [ + { + "outPoint": { + "txHash": "0x27b62d8be8ed80b9f56ee0fe41355becdb6f6a40aeba82d3900434f43b1c8b60", + "index": "0x0" + }, + "depType": "code" + }, + { + "outPoint": { + "txHash": "0xf8de3bb47d055cdf460d93a2a6e1b05f7432f9777c8c474abf4eec1d4aee5d37", + "index": "0x0" + }, + "depType": "depGroup" + }, + { + "outPoint": { + "txHash": "0xe12877ebd2c3c364dc46c5c992bcfaf4fee33fa13eebdf82c591fc9825aab769", + "index": "0x0" + }, + "depType": "code" + } + ], + "headerDeps": [], + "inputs": [ + { + "cellOutput": { + "capacity": "0x1b41bf852c00", + "lock": { + "codeHash": "0xf329effd1c475a2978453c8600e1eaf0bc2087ee093c3ee64cc96ec6847752cb", + "hashType": "type", + "args": "0x00afbf535944be46a2f5879a3a349bc4fd5784a0e900" + }, + "type": null + }, + "data": "0x", + "outPoint": { + "txHash": "0x5f2d84f67f378972ba7ee285e4d013450862d31defc121769fbf61fd5810627d", + "index": "0x1" + }, + "blockNumber": "0xa66258" + } + ], + "outputs": [ + { + "cellOutput": { + "capacity": "0x35a4e9000", + "lock": { + "codeHash": "0xf329effd1c475a2978453c8600e1eaf0bc2087ee093c3ee64cc96ec6847752cb", + "hashType": "type", + "args": "0x00afbf535944be46a2f5879a3a349bc4fd5784a0e900" + }, + "type": { + "codeHash": "0xc5e5dcf215925f7ef4dfaf5f4b4f105bc321c02776d6e7d52a1db3fcd9d011a4", + "hashType": "type", + "args": "0xfb7b6c4a2baf39ebfdd634e76737725362cf18042a31256488382137ae830784" + } + }, + "data": "0xa0860100000000000000000000000000" + }, + { + "cellOutput": { + "lock": { + "codeHash": "0xf329effd1c475a2978453c8600e1eaf0bc2087ee093c3ee64cc96ec6847752cb", + "hashType": "type", + "args": "0x00afbf535944be46a2f5879a3a349bc4fd5784a0e900" + }, + "capacity": "0x1b3e65351560" + }, + "data": "0x" + } + ], + "witnesses": [ + "0x690000001000000069000000690000005500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ], + "fixedEntries": [], + "signingEntries": [], + "inputSinces": {} + } + } +} +``` + ### Create Token path: /token @@ -24,17 +131,13 @@ method: POST { "code": 200, "data": { - "symbol": "USDT", "name": "USDT", - "account": "", // the args of account in omnilock - "supply": "100000", + "account": "", // the address of owner "decimal": 18, "description": "", "website": "", "icon": "", - "typeId": "", - "args": "", // the args of sudt type script - "explorerCode": "" // the verify code from explorer + "email": "" } } ``` @@ -43,16 +146,96 @@ method: POST ```javascript { - "code": 201, - "data": { - "url": "" // direct to explorer to the transaction to issue the token + "code": "201", + "data": { + "txSkeleton": { + "cellProvider": null, + "cellDeps": [ + { + "outPoint": { + "txHash": "0x27b62d8be8ed80b9f56ee0fe41355becdb6f6a40aeba82d3900434f43b1c8b60", + "index": "0x0" + }, + "depType": "code" + }, + { + "outPoint": { + "txHash": "0xf8de3bb47d055cdf460d93a2a6e1b05f7432f9777c8c474abf4eec1d4aee5d37", + "index": "0x0" + }, + "depType": "depGroup" + }, + { + "outPoint": { + "txHash": "0xe12877ebd2c3c364dc46c5c992bcfaf4fee33fa13eebdf82c591fc9825aab769", + "index": "0x0" + }, + "depType": "code" + } + ], + "headerDeps": [], + "inputs": [ + { + "cellOutput": { + "capacity": "0x1b41bf852c00", + "lock": { + "codeHash": "0xf329effd1c475a2978453c8600e1eaf0bc2087ee093c3ee64cc96ec6847752cb", + "hashType": "type", + "args": "0x00afbf535944be46a2f5879a3a349bc4fd5784a0e900" + }, + "type": null + }, + "data": "0x", + "outPoint": { + "txHash": "0x5f2d84f67f378972ba7ee285e4d013450862d31defc121769fbf61fd5810627d", + "index": "0x1" + }, + "blockNumber": "0xa66258" + } + ], + "outputs": [ + { + "cellOutput": { + "capacity": "0x35a4e9000", + "lock": { + "codeHash": "0xf329effd1c475a2978453c8600e1eaf0bc2087ee093c3ee64cc96ec6847752cb", + "hashType": "type", + "args": "0x00afbf535944be46a2f5879a3a349bc4fd5784a0e900" + }, + "type": { + "codeHash": "0xc5e5dcf215925f7ef4dfaf5f4b4f105bc321c02776d6e7d52a1db3fcd9d011a4", + "hashType": "type", + "args": "0xfb7b6c4a2baf39ebfdd634e76737725362cf18042a31256488382137ae830784" + } + }, + "data": "0xa0860100000000000000000000000000" + }, + { + "cellOutput": { + "lock": { + "codeHash": "0xf329effd1c475a2978453c8600e1eaf0bc2087ee093c3ee64cc96ec6847752cb", + "hashType": "type", + "args": "0x00afbf535944be46a2f5879a3a349bc4fd5784a0e900" + }, + "capacity": "0x1b3e65351560" + }, + "data": "0x" + } + ], + "witnesses": [ + "0x690000001000000069000000690000005500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ], + "fixedEntries": [], + "signingEntries": [], + "inputSinces": {} + } } } ``` ### Update Token -path: /token +path: /token/:typeId method: PUT @@ -62,15 +245,12 @@ method: PUT { "code": 200, "data": { - "symbol": "USDT", "name": "USDT", - "amount": "100000", - "decimal": "18", + "decimal": 18, "description": "", "website": "", "icon": "", - "args": "", // sudt args - "signature": "" + "explorerCode": "" // the verify code from explorer } } ``` @@ -92,13 +272,101 @@ method: POST #### Request +```javascript +{ + "typeId": "", // token args + "amount": "", + "to": "" +} +``` + +#### Response + ```javascript { "code": 200, "data": { - "token": "", // token args - "amount": "", - "to": "" + "txSkeleton": { + "cellProvider": null, + "cellDeps": [ + { + "outPoint": { + "txHash": "0x27b62d8be8ed80b9f56ee0fe41355becdb6f6a40aeba82d3900434f43b1c8b60", + "index": "0x0" + }, + "depType": "code" + }, + { + "outPoint": { + "txHash": "0xf8de3bb47d055cdf460d93a2a6e1b05f7432f9777c8c474abf4eec1d4aee5d37", + "index": "0x0" + }, + "depType": "depGroup" + }, + { + "outPoint": { + "txHash": "0xe12877ebd2c3c364dc46c5c992bcfaf4fee33fa13eebdf82c591fc9825aab769", + "index": "0x0" + }, + "depType": "code" + } + ], + "headerDeps": [], + "inputs": [ + { + "cellOutput": { + "capacity": "0x1b41bf852c00", + "lock": { + "codeHash": "0xf329effd1c475a2978453c8600e1eaf0bc2087ee093c3ee64cc96ec6847752cb", + "hashType": "type", + "args": "0x00afbf535944be46a2f5879a3a349bc4fd5784a0e900" + }, + "type": null + }, + "data": "0x", + "outPoint": { + "txHash": "0x5f2d84f67f378972ba7ee285e4d013450862d31defc121769fbf61fd5810627d", + "index": "0x1" + }, + "blockNumber": "0xa66258" + } + ], + "outputs": [ + { + "cellOutput": { + "capacity": "0x35a4e9000", + "lock": { + "codeHash": "0xf329effd1c475a2978453c8600e1eaf0bc2087ee093c3ee64cc96ec6847752cb", + "hashType": "type", + "args": "0x00afbf535944be46a2f5879a3a349bc4fd5784a0e900" + }, + "type": { + "codeHash": "0xc5e5dcf215925f7ef4dfaf5f4b4f105bc321c02776d6e7d52a1db3fcd9d011a4", + "hashType": "type", + "args": "0xfb7b6c4a2baf39ebfdd634e76737725362cf18042a31256488382137ae830784" + } + }, + "data": "0xa0860100000000000000000000000000" + }, + { + "cellOutput": { + "lock": { + "codeHash": "0xf329effd1c475a2978453c8600e1eaf0bc2087ee093c3ee64cc96ec6847752cb", + "hashType": "type", + "args": "0x00afbf535944be46a2f5879a3a349bc4fd5784a0e900" + }, + "capacity": "0x1b3e65351560" + }, + "data": "0x" + } + ], + "witnesses": [ + "0x690000001000000069000000690000005500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ], + "fixedEntries": [], + "signingEntries": [], + "inputSinces": {} + } } } ``` @@ -122,13 +390,17 @@ method: GET "code": 200, "data": [ { - symbol: 'USDT', - name: 'USDT', - amount: '100000', - decimal: '18', - description: '', - website: '', - icon: '', + "uan": "USDT", + "displayName": "USDT", + "name": "USDT", + "decimal": 18, + "description": "", + "website": "", + "icon": "", + "url": "", + "issuser": "", + "args": "", + "typeId": "", }, ] } @@ -146,10 +418,10 @@ method: GET { "code": 200, "data": { - "symbol": "USDT", + "uan": "USDT", + "displayName": "USDT", "name": "USDT", - "amount": "100000", - "decimal": "18", + "decimal": 18, "description": "", "website": "", "icon": "", @@ -178,8 +450,9 @@ method: GET "code": 200, "data": [ { - "symbol": "USDT", - "name": "USDT", + "uan": "USDT", + "displayName": "USDT", + "decimal": 18, "amount": "" } ] From c8b5e68fc1b327be7935c659ad5cb40c82c2479e Mon Sep 17 00:00:00 2001 From: daryl Date: Tue, 24 Oct 2023 17:17:26 +0800 Subject: [PATCH 11/49] feat(sudt): token apis 1. create token 2. update token --- .../samples/sudt/src/actors/omnilock.model.ts | 19 +++--- .../sudt/src/controllers/sudt.controller.ts | 64 +++++++++++++++---- .../samples/sudt/src/dto/create-token.dto.ts | 8 +-- packages/samples/sudt/src/dto/mint.dto.ts | 12 ++++ .../samples/sudt/src/entities/token.entity.ts | 8 +++ 5 files changed, 86 insertions(+), 25 deletions(-) create mode 100644 packages/samples/sudt/src/dto/mint.dto.ts diff --git a/packages/samples/sudt/src/actors/omnilock.model.ts b/packages/samples/sudt/src/actors/omnilock.model.ts index 41ad7076..167b5510 100644 --- a/packages/samples/sudt/src/actors/omnilock.model.ts +++ b/packages/samples/sudt/src/actors/omnilock.model.ts @@ -4,7 +4,7 @@ * This is the actor model for omnilock, which is used to gather omnilock cells to generate record models. */ -import type { Cell, HexString, Script } from '@ckb-lumos/base' +import type { Cell, Script } from '@ckb-lumos/base' import { ActorProvider, Omnilock, @@ -60,25 +60,27 @@ export class OmnilockModel extends JSONStore> { mint( lockScript: Script, - amount: HexString, + amount: BI, args?: string, ): { inputs: Cell[] outputs: Cell[] witnesses: string[] + typeScript: Script } { const CONFIG = getConfig() + const typeScript = { + codeHash: CONFIG.SCRIPTS.SUDT!.CODE_HASH, + hashType: CONFIG.SCRIPTS.SUDT!.HASH_TYPE, + args: args ?? utils.computeScriptHash(lockScript), + } const sudtCell: Cell = { cellOutput: { capacity: BI.from(MIN_SUDT_WITH_OMINILOCK).toHexString(), lock: lockScript, - type: { - codeHash: CONFIG.SCRIPTS.SUDT!.CODE_HASH, - hashType: CONFIG.SCRIPTS.SUDT!.HASH_TYPE, - args: args ?? utils.computeScriptHash(lockScript), - }, + type: typeScript, }, - data: bytes.hexify(number.Uint128LE.pack(amount)), + data: bytes.hexify(number.Uint128LE.pack(amount.toHexString())), } const cells = Object.values(this.chainData) let currentTotalCapacity: BI = BI.from(0) @@ -93,6 +95,7 @@ export class OmnilockModel extends JSONStore> { if (currentTotalCapacity.lt(needCapacity)) throw new InternalServerError('not enough capacity') return { + typeScript, inputs: inputs.map((v) => v.cell), outputs: [ sudtCell, diff --git a/packages/samples/sudt/src/controllers/sudt.controller.ts b/packages/samples/sudt/src/controllers/sudt.controller.ts index dc6de30c..d044eef3 100644 --- a/packages/samples/sudt/src/controllers/sudt.controller.ts +++ b/packages/samples/sudt/src/controllers/sudt.controller.ts @@ -1,7 +1,7 @@ import type { HexString, Hash } from '@ckb-lumos/base' import { ActorReference } from '@ckb-js/kuai-models' import { BadRequest, NotFound } from 'http-errors' -import { SudtModel, appRegistry } from '../actors' +import { OmnilockModel, SudtModel, appRegistry } from '../actors' import { Tx } from '../views/tx.view' import { getLock } from '../utils' import { BaseController, Body, Controller, Get, Param, Post, Put } from '@ckb-js/kuai-io' @@ -11,11 +11,16 @@ import { DataSource, QueryFailedError } from 'typeorm' import { Token } from '../entities/token.entity' import { Account } from '../entities/account.entity' import { tokenEntityToDto } from '../dto/token.dto' +import { ExplorerService } from '../services/explorer.service' +import { BI, utils } from '@ckb-lumos/lumos' @Controller('sudt') export default class SudtController extends BaseController { #explorerHost = process.env.EXPLORER_HOST || 'https://explorer.nervos.org' - constructor(private _dataSource: DataSource) { + constructor( + private _dataSource: DataSource, + private _explorerService: ExplorerService, + ) { super() } @@ -81,11 +86,26 @@ export default class SudtController extends BaseController { .save(this._dataSource.getRepository(Account).create({ address: req.account })) } + const amount = BI.isBI(req.amount) ? BI.from(req.amount) : BI.from(0) 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}` }) + const omniLockModel = appRegistry.findOrBind( + new ActorReference('omnilock', `/${getLock(req.account).args}/`), + ) + const { typeScript, ...result } = omniLockModel.mint(getLock(req.account), amount) + + await this._dataSource.getRepository(Token).save( + this._dataSource.getRepository(Token).create({ + name: req.name, + ownerId: owner.id, + decimal: req.decimal, + description: req.description, + website: req.website, + icon: req.icon, + args: typeScript.args, + typeId: utils.computeScriptHash(typeScript), + }), + ) + return new SudtResponse('201', Tx.toJsonString(result)) } catch (e) { if (e instanceof QueryFailedError) { switch (e.driverError.code) { @@ -95,17 +115,37 @@ export default class SudtController extends BaseController { } console.error(e) + throw 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 }) + @Put('/token/:typeId') + async updateToken(@Body() req: CreateTokenRequest, @Param('typeId') typeId: string) { + const token = await this._dataSource.getRepository(Token).findOneBy({ typeId }) + if (!token) { + return SudtResponse.err('404', { message: 'Token not found' }) } - return new SudtResponse('201', {}) + try { + await this._explorerService.updateSUDT({ + typeHash: typeId, + symbol: req.name, + fullName: req.name, + decimal: req.decimal.toString(), + totalAmount: '0', + description: req.description, + operatorWebsite: req.website, + iconFile: req.icon, + uan: `${req.name}.ckb`, + displayName: req.name, + email: req.email, + token: req.explorerCode, + }) + await this._dataSource.getRepository(Token).save({ ...token, ...req }) + return new SudtResponse('201', {}) + } catch (e) { + throw SudtResponse.err('500', { message: (e as Error).message }) + } } @Get('/token/:typeId') diff --git a/packages/samples/sudt/src/dto/create-token.dto.ts b/packages/samples/sudt/src/dto/create-token.dto.ts index 84156617..ec8ee3b8 100644 --- a/packages/samples/sudt/src/dto/create-token.dto.ts +++ b/packages/samples/sudt/src/dto/create-token.dto.ts @@ -1,15 +1,13 @@ export interface CreateTokenRequest { name: string - symbol: string - supply: string account: string decimal: number description: string website: string icon: string - typeId: string - explorerCode: string - args: string + amount: string + email: string + explorerCode?: string } export interface CreateTokenResponse { diff --git a/packages/samples/sudt/src/dto/mint.dto.ts b/packages/samples/sudt/src/dto/mint.dto.ts new file mode 100644 index 00000000..6fb98ee9 --- /dev/null +++ b/packages/samples/sudt/src/dto/mint.dto.ts @@ -0,0 +1,12 @@ +import { TransactionSkeletonType } from '@ckb-lumos/helpers' +import { HexString } from '@ckb-lumos/lumos' + +export interface MintRequest { + from: string[] + to: string + amount: HexString +} + +export interface MintResponse { + txSkeleton: TransactionSkeletonType +} diff --git a/packages/samples/sudt/src/entities/token.entity.ts b/packages/samples/sudt/src/entities/token.entity.ts index 35b8e290..15678642 100644 --- a/packages/samples/sudt/src/entities/token.entity.ts +++ b/packages/samples/sudt/src/entities/token.entity.ts @@ -1,5 +1,10 @@ import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn, Unique, UpdateDateColumn } from 'typeorm' +export enum TokenStatus { + New = 1, + Committed, +} + @Entity() export class Token { @PrimaryGeneratedColumn() @@ -35,6 +40,9 @@ export class Token { @Unique('uniq_args', ['args']) args!: string + @Column({ default: TokenStatus.New }) + status!: TokenStatus + @CreateDateColumn() createdAt!: Date From 609032b1ec2c054afc26e9ece99de40b381df082 Mon Sep 17 00:00:00 2001 From: daryl Date: Tue, 24 Oct 2023 17:23:31 +0800 Subject: [PATCH 12/49] feat(sudt): example of env file --- packages/samples/sudt/.env.example | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 packages/samples/sudt/.env.example diff --git a/packages/samples/sudt/.env.example b/packages/samples/sudt/.env.example new file mode 100644 index 00000000..3225f629 --- /dev/null +++ b/packages/samples/sudt/.env.example @@ -0,0 +1,19 @@ +# app +HOST=127.0.0.1 +PORT=3001 + +# ckb +CKB_RPC_URL=https://testnet.ckb.dev/rpc +NETWORK=testnet + +# redis +REDIS_PORT=6379 +REDIS_HOST=127.0.0.1 + +EXPLORER_HOST=https://explorer.nervos.org +EXPLORER_API_HOST=https://explorer.nervos.org/api +DB_HOST=localhost +DB_PORT=3306 +DB_USERNAME=root +DB_PASSWORD=root +DB_DATABASE=sudt \ No newline at end of file From e2370fef5b02e3e32e5b59a917d477cbe749216e Mon Sep 17 00:00:00 2001 From: daryl Date: Tue, 24 Oct 2023 17:26:31 +0800 Subject: [PATCH 13/49] fix(sudt): amount should use BI --- .../src/controllers/account.controller.ts | 3 +- .../src/controllers/omnilock.controller.ts | 37 ------------------- 2 files changed, 2 insertions(+), 38 deletions(-) delete mode 100644 packages/samples/sudt/src/controllers/omnilock.controller.ts diff --git a/packages/samples/sudt/src/controllers/account.controller.ts b/packages/samples/sudt/src/controllers/account.controller.ts index e5d3ac10..c9d04a82 100644 --- a/packages/samples/sudt/src/controllers/account.controller.ts +++ b/packages/samples/sudt/src/controllers/account.controller.ts @@ -10,6 +10,7 @@ import { Token } from '../entities/token.entity' import { BadRequest } from 'http-errors' import { Tx } from '../views/tx.view' import { MintRequest } from '../dto/mint.dto' +import { BI } from '@ckb-lumos/lumos' @Controller('/account') export class AccountController extends BaseController { @@ -33,7 +34,7 @@ export class AccountController extends BaseController { new ActorReference('omnilock', `/${getLock(from[0]).args}/`), ) - const result = omniLockModel.mint(getLock(to), amount, token.args) + const result = omniLockModel.mint(getLock(to), BI.isBI(amount) ? BI.from(amount) : BI.from(0), token.args) return SudtResponse.ok(await Tx.toJsonString(result)) } diff --git a/packages/samples/sudt/src/controllers/omnilock.controller.ts b/packages/samples/sudt/src/controllers/omnilock.controller.ts deleted file mode 100644 index 7a2c9904..00000000 --- a/packages/samples/sudt/src/controllers/omnilock.controller.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { HexString } from '@ckb-lumos/base' -import { BaseController, Controller, Get, Param, Post, Body } from '@ckb-js/kuai-io' -import { ActorReference } from '@ckb-js/kuai-models' -import { BadRequest } from 'http-errors' -import { OmnilockModel, appRegistry } from '../actors' -import { getLock } from '../utils' -import { Tx } from '../views/tx.view' -import { SudtResponse } from '../response' - -@Controller('omnilock') -export default class OmnilockController extends BaseController { - @Get('/meta/:address') - async meta(@Param('address') address: string) { - if (!address) { - throw new BadRequest('invalid address') - } - - const omniLockModel = appRegistry.findOrBind( - new ActorReference('omnilock', `/${getLock(address).args}/`), - ) - - return SudtResponse.ok(omniLockModel?.meta) - } - - @Post('/mint') - async mint(@Body() { from, to, amount }: { from: string; to: string; amount: HexString }) { - if (!from || !to || !amount) { - throw new BadRequest('undefined body field: from, to or amount') - } - - const omniLockModel = appRegistry.findOrBind( - new ActorReference('omnilock', `/${getLock(from).args}/`), - ) - const result = omniLockModel.mint(getLock(to), amount) - return SudtResponse.ok(await Tx.toJsonString(result)) - } -} From f09f908fcb7bf89d2883bb45c36ecc42ee5b4d6f Mon Sep 17 00:00:00 2001 From: daryl Date: Wed, 25 Oct 2023 11:04:49 +0800 Subject: [PATCH 14/49] feat(sudt): add explorer service --- .../sudt/src/services/explorer.service.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 packages/samples/sudt/src/services/explorer.service.ts diff --git a/packages/samples/sudt/src/services/explorer.service.ts b/packages/samples/sudt/src/services/explorer.service.ts new file mode 100644 index 00000000..e000bcc6 --- /dev/null +++ b/packages/samples/sudt/src/services/explorer.service.ts @@ -0,0 +1,32 @@ +export class ExplorerService { + constructor(private host = '') {} + + updateSUDT = async (params: { + typeHash: string + symbol: string + fullName: string + decimal: string + totalAmount: string + description: string + operatorWebsite: string + iconFile: string + uan: string + displayName: string + email: string + token?: string + }) => { + const res = await fetch(`${this.host}/api/v1/udts/${params.typeHash}`, { method: 'POST', body: JSON.stringify({}) }) + if (!res.ok) { + throw new Error('Internal Service Error') + } + + switch (Math.ceil(res.status / 100)) { + case 2: + return true + case 3: + throw new Error('Redirect') + case 4: + throw new Error('Client Error') + } + } +} From 0c3b3bc7d36a3a6bb9168b8901b66cc2b57837e0 Mon Sep 17 00:00:00 2001 From: daryl Date: Wed, 25 Oct 2023 11:20:41 +0800 Subject: [PATCH 15/49] feat(sudt): explorer service --- packages/samples/sudt/src/bootstrap.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/samples/sudt/src/bootstrap.ts b/packages/samples/sudt/src/bootstrap.ts index 91bf0eac..5e1d7254 100644 --- a/packages/samples/sudt/src/bootstrap.ts +++ b/packages/samples/sudt/src/bootstrap.ts @@ -13,6 +13,7 @@ import { import { config } from '@ckb-lumos/lumos' import { DataSource } from 'typeorm' import { AccountController } from './controllers/account.controller' +import { ExplorerService } from './services/explorer.service' const initiateDataSource = async () => { const dataSource = new DataSource({ @@ -68,7 +69,7 @@ export const bootstrap = async () => { // init kuai io const cor = new CoR() - const sudtController = new SudtController(dataSource) + const sudtController = new SudtController(dataSource, new ExplorerService()) const accountController = new AccountController(dataSource) cor.use(sudtController.middleware()) cor.use(accountController.middleware()) From ef557cbdded677ddde0474532702446a09cf1ce9 Mon Sep 17 00:00:00 2001 From: daryl Date: Thu, 23 Nov 2023 02:24:41 +0800 Subject: [PATCH 16/49] feat(sudt-manager): api --- packages/samples/sudt/kuai.config.ts | 28 ++++------ .../samples/sudt/src/actors/omnilock.model.ts | 24 +++++--- .../samples/sudt/src/actors/sudt.model.ts | 32 +++++------ packages/samples/sudt/src/bootstrap.ts | 44 +++++++++------ .../src/controllers/account.controller.ts | 41 ++++++++++++-- .../sudt/src/controllers/sudt.controller.ts | 56 +++++++++++-------- .../samples/sudt/src/entities/asset.entity.ts | 32 +++++++++++ .../sudt/src/services/explorer.service.ts | 11 +++- .../samples/sudt/src/tasks/balance.task.ts | 55 ++++++++++++++++++ 9 files changed, 231 insertions(+), 92 deletions(-) create mode 100644 packages/samples/sudt/src/entities/asset.entity.ts create mode 100644 packages/samples/sudt/src/tasks/balance.task.ts diff --git a/packages/samples/sudt/kuai.config.ts b/packages/samples/sudt/kuai.config.ts index 665f0ab1..88f662de 100644 --- a/packages/samples/sudt/kuai.config.ts +++ b/packages/samples/sudt/kuai.config.ts @@ -1,26 +1,25 @@ -import { KuaiConfig } from '@ckb-js/kuai-core'; - -let redisOpt = undefined; +let redisOpt = undefined if (process.env.REDIS_OPT) { try { - redisOpt = JSON.parse(process.env.REDIS_OPT); + redisOpt = JSON.parse(process.env.REDIS_OPT) } catch (error) { //ignore error, if error redisOpt will be undefined } } // fallback to REDISUSER due to https://github.com/ckb-js/kuai/pull/423#issuecomment-1668983983 -const REDIS_USER = redisOpt?.username ?? process.env.REDISUSER; -const REDIS_PASSWORD = redisOpt?.password ?? process.env.REDISPASSWORD; -const REDIS_HOST = process.env.REDIS_HOST ?? process.env.REDISHOST; -const REDIS_PORT = process.env.REDIS_PORT ?? process.env.REDISPORT; +const REDIS_USER = redisOpt?.username ?? process.env.REDISUSER +const REDIS_PASSWORD = redisOpt?.password ?? process.env.REDISPASSWORD +const REDIS_HOST = process.env.REDIS_HOST ?? process.env.REDISHOST +const REDIS_PORT = process.env.REDIS_PORT ?? process.env.REDISPORT -const redisAuth = REDIS_USER && REDIS_PASSWORD ? { username: REDIS_USER, password: REDIS_PASSWORD } : undefined; +const redisAuth = REDIS_USER && REDIS_PASSWORD ? { username: REDIS_USER, password: REDIS_PASSWORD } : undefined -const config: KuaiConfig = { +const config = { port: 3000, redisPort: REDIS_HOST ? +REDIS_HOST : undefined, redisHost: REDIS_PORT, + network: process.env.NETWORK || 'testnet', redisOpt: redisOpt || redisAuth ? { @@ -28,11 +27,6 @@ const config: KuaiConfig = { ...redisAuth, } : undefined, - network: 'testnet', - ckbChain: { - rpcUrl: 'http://127.0.0.1:8114', - prefix: 'ckt', - }, -}; +} -export default config; +export default config diff --git a/packages/samples/sudt/src/actors/omnilock.model.ts b/packages/samples/sudt/src/actors/omnilock.model.ts index 167b5510..236b9623 100644 --- a/packages/samples/sudt/src/actors/omnilock.model.ts +++ b/packages/samples/sudt/src/actors/omnilock.model.ts @@ -58,6 +58,20 @@ export class OmnilockModel extends JSONStore> { } } + loadCapacity = (capacity: BI) => { + const cells = Object.values(this.chainData) + let currentTotalCapacity = BI.from(0) + const inputs = cells.filter((v) => { + if (currentTotalCapacity.gte(capacity)) return false + currentTotalCapacity = currentTotalCapacity.add(BI.from(v.cell.cellOutput.capacity)) + return true + }) + + if (currentTotalCapacity.lt(capacity)) throw new InternalServerError('not enough capacity') + + return { inputs, currentTotalCapacity } + } + mint( lockScript: Script, amount: BI, @@ -82,17 +96,9 @@ export class OmnilockModel extends JSONStore> { }, data: bytes.hexify(number.Uint128LE.pack(amount.toHexString())), } - const cells = Object.values(this.chainData) - let currentTotalCapacity: BI = BI.from(0) // additional 0.001 ckb for tx fee const needCapacity = BI.from(sudtCell.cellOutput.capacity).add(TX_FEE) - const inputs = cells.filter((v) => { - if (v.cell.cellOutput.type) return false - if (currentTotalCapacity.gte(needCapacity)) return false - currentTotalCapacity = currentTotalCapacity.add(BI.from(v.cell.cellOutput.capacity)) - return true - }) - if (currentTotalCapacity.lt(needCapacity)) throw new InternalServerError('not enough capacity') + const { inputs, currentTotalCapacity } = this.loadCapacity(needCapacity) return { typeScript, diff --git a/packages/samples/sudt/src/actors/sudt.model.ts b/packages/samples/sudt/src/actors/sudt.model.ts index 352da589..93979e4b 100644 --- a/packages/samples/sudt/src/actors/sudt.model.ts +++ b/packages/samples/sudt/src/actors/sudt.model.ts @@ -23,11 +23,12 @@ import { number, bytes } from '@ckb-lumos/codec' import { InternalServerError } from 'http-errors' import { BI, utils, config } from '@ckb-lumos/lumos' import { MIN_SUDT_WITH_OMINILOCK, TX_FEE } from '../const' +import { OmnilockModel } from './omnilock.model' /** * add business logic in an actor */ -@ActorProvider({ ref: { name: 'sudt', path: `/:args/` } }) +@ActorProvider({ ref: { name: 'sudt', path: `/:typeArgs/:lockArgs/` } }) @TypeFilter() @LockFilter() @Omnilock() @@ -44,30 +45,27 @@ export class SudtModel extends JSONStore> { schemaPattern?: SchemaPattern }, ) { - super(undefined, { ...params, ref: ActorReference.newWithFilter(SudtModel, `/${lockArgs}/${typeArgs}/`) }) + super(undefined, { ...params, ref: ActorReference.newWithFilter(SudtModel, `/${typeArgs}/${lockArgs}/`) }) if (!this.typeScript) { throw new Error('type script is required') } this.registerResourceBinding() } - getSudtBalance(lockScripts: Script[]): Record<'capacity' | 'sudtBalance', string> { - let capacity = BigInt(0) - let sudtBalance = BigInt(0) - const filterScriptHashes = new Set(lockScripts.map((v) => utils.computeScriptHash(v))) + getSudtBalance(): Record<'capacity' | 'sudtBalance', BI> { + let capacity = BI.from(0) + let sudtBalance = BI.from(0) Object.values(this.chainData).forEach((v) => { - if (filterScriptHashes.has(utils.computeScriptHash(v.cell.cellOutput.lock))) { - capacity += BigInt(v.cell.cellOutput.capacity ?? 0) - sudtBalance += number.Uint128LE.unpack(v.cell.data.slice(0, 34)).toBigInt() - } + capacity = capacity.add(v.cell.cellOutput.capacity ?? 0) + sudtBalance = sudtBalance.add(number.Uint128LE.unpack(v.cell.data.slice(0, 34))) }) return { - capacity: capacity.toString(), - sudtBalance: sudtBalance.toString(), + capacity: capacity, + sudtBalance: sudtBalance, } } - send(from: Script[], lockScript: Script, amount: HexString) { + send(omnilock: OmnilockModel, lockScript: Script, amount: HexString) { const CONFIG = config.getConfig() const sudtCell: Cell = { cellOutput: { @@ -86,15 +84,15 @@ export class SudtModel extends JSONStore> { let currentTotalCapacity: BI = BI.from(0) // additional 0.001 ckb for tx fee const needCapacity = BI.from(sudtCell.cellOutput.capacity).add(TX_FEE) - const fromScriptHashes = new Set(from.map((v) => utils.computeScriptHash(v))) - const inputs = cells.filter((v) => { - if (!fromScriptHashes.has(utils.computeScriptHash(v.cell.cellOutput.lock))) return false + let inputs = cells.filter((v) => { if (currentTotalCapacity.gte(needCapacity) && currentTotalSudt.gte(amount)) return false currentTotalCapacity = currentTotalCapacity.add(BI.from(v.cell.cellOutput.capacity)) currentTotalSudt = currentTotalSudt.add(number.Uint128LE.unpack(v.cell.data.slice(0, 34))) return true }) - if (currentTotalCapacity.lt(needCapacity)) throw new InternalServerError('not enough capacity') + if (currentTotalCapacity.lt(needCapacity)) { + inputs = inputs.concat(omnilock.loadCapacity(needCapacity.sub(currentTotalCapacity))) + } if (currentTotalSudt.lt(amount)) throw new InternalServerError('not enough sudt balance') const leftSudt = currentTotalSudt.sub(amount) diff --git a/packages/samples/sudt/src/bootstrap.ts b/packages/samples/sudt/src/bootstrap.ts index 5e1d7254..6cecb19d 100644 --- a/packages/samples/sudt/src/bootstrap.ts +++ b/packages/samples/sudt/src/bootstrap.ts @@ -14,6 +14,7 @@ import { config } from '@ckb-lumos/lumos' import { DataSource } from 'typeorm' import { AccountController } from './controllers/account.controller' import { ExplorerService } from './services/explorer.service' +import { BalanceTask } from './tasks/balance.task' const initiateDataSource = async () => { const dataSource = new DataSource({ @@ -67,9 +68,12 @@ export const bootstrap = async () => { const dataSource = await initiateDataSource() + const balanceTask = new BalanceTask(dataSource) + balanceTask.run() + // init kuai io const cor = new CoR() - const sudtController = new SudtController(dataSource, new ExplorerService()) + const sudtController = new SudtController(dataSource, new ExplorerService(process.env.EXPLORER_API_HOST)) const accountController = new AccountController(dataSource) cor.use(sudtController.middleware()) cor.use(accountController.middleware()) @@ -78,20 +82,26 @@ export const bootstrap = async () => { app.use(koaRouterAdapter.routes()) - const server = app.listen(port, function () { - const address = (() => { - const _address = server.address() - if (!_address) { - return '' - } - - if (typeof _address === 'string') { - return _address - } - - return `http://${_address.address}:${_address.port}` - })() - - console.log(`kuai app listening at ${address}`) - }) + // while (true) { + try { + const server = app.listen(port, function () { + const address = (() => { + const _address = server.address() + if (!_address) { + return '' + } + + if (typeof _address === 'string') { + return _address + } + + return `http://${_address.address}:${_address.port}` + })() + + console.log(`kuai app listening at ${address}`) + }) + } catch (e) { + console.error(e) + } + // } } diff --git a/packages/samples/sudt/src/controllers/account.controller.ts b/packages/samples/sudt/src/controllers/account.controller.ts index c9d04a82..029f0552 100644 --- a/packages/samples/sudt/src/controllers/account.controller.ts +++ b/packages/samples/sudt/src/controllers/account.controller.ts @@ -2,7 +2,7 @@ import { BaseController, Body, Controller, Get, Param, Post } from '@ckb-js/kuai import { DataSource } from 'typeorm' import { Transaction } from '../entities/transaction.entity' import { Account } from '../entities/account.entity' -import { OmnilockModel, SudtModel, appRegistry } from '../actors' +import { OmnilockModel, appRegistry } from '../actors' import { ActorReference } from '@ckb-js/kuai-models' import { getLock } from '../utils' import { SudtResponse } from '../response' @@ -11,6 +11,7 @@ import { BadRequest } from 'http-errors' import { Tx } from '../views/tx.view' import { MintRequest } from '../dto/mint.dto' import { BI } from '@ckb-lumos/lumos' +import { Asset } from '../entities/asset.entity' @Controller('/account') export class AccountController extends BaseController { @@ -81,18 +82,46 @@ export class AccountController extends BaseController { @Get('/:address/assets') async accountAssets(@Param('address') address: string) { const tokens = await this._dataSource.getRepository(Token).find() - const lock = getLock(address) + const account = await this._dataSource.getRepository(Account).findOneBy({ address }) + if (!account) { + throw SudtResponse.err(404, 'account not found') + } + + const assets = await this._dataSource.getRepository(Asset).findBy({ accountId: account.id }) + const assetsMap = assets.reduce((acc, cur) => { + acc.set(cur.tokenId, cur) + return acc + }, new Map()) + console.log(assetsMap) return tokens.map((token) => { try { return { - ...token, - ...appRegistry - .findOrBind(new ActorReference('sudt', `/${lock.args}/${token.args}/`)) - .getSudtBalance([getLock(address)]), + uan: token.name, + displayName: token.name, + decimal: token.decimal, + amount: assetsMap.get(token.id)?.balance ?? '0', } } catch (e) { console.error(e) } }) } + + // @Post('/transfer/:typeId') + // async transfer(@Param('tokenId') typeId: string, @Body() { from, to, amount }: MintRequest) { + // if (!from || from.length === 0 || !to || !amount) { + // throw new BadRequest('undefined body field: from, to or amount') + // } + + // const token = await this._dataSource.getRepository(Token).findOneBy({ typeId }) + // if (!token) { + // return SudtResponse.err(404, 'token not found') + // } + + // const sudt = appRegistry.findOrBind(new ActorReference('sudt', `/${token.args}/`)) + + // const result = sudt.send(getLock(to), amount) + + // return Tx.toJsonString(result) + // } } diff --git a/packages/samples/sudt/src/controllers/sudt.controller.ts b/packages/samples/sudt/src/controllers/sudt.controller.ts index d044eef3..fb2b9273 100644 --- a/packages/samples/sudt/src/controllers/sudt.controller.ts +++ b/packages/samples/sudt/src/controllers/sudt.controller.ts @@ -13,8 +13,9 @@ import { Account } from '../entities/account.entity' import { tokenEntityToDto } from '../dto/token.dto' import { ExplorerService } from '../services/explorer.service' import { BI, utils } from '@ckb-lumos/lumos' +import { MintRequest } from '../dto/mint.dto' -@Controller('sudt') +@Controller('token') export default class SudtController extends BaseController { #explorerHost = process.env.EXPLORER_HOST || 'https://explorer.nervos.org' constructor( @@ -35,31 +36,38 @@ export default class SudtController extends BaseController { return SudtResponse.ok(sudtModel.meta()) } - @Post('/getSudtBalance') - async getSudtBalance(@Body() { addresses, typeArgs }: { addresses: string[]; typeArgs: Hash }) { - if (!addresses?.length || !typeArgs) { - throw new BadRequest('undefined body field: from or typeArgs') - } + // @Post('/getSudtBalance') + // async getSudtBalance(@Body() { addresses, typeArgs }: { addresses: string[]; typeArgs: Hash }) { + // if (!addresses?.length || !typeArgs) { + // throw new BadRequest('undefined body field: from or typeArgs') + // } - const sudtModel = appRegistry.findOrBind(new ActorReference('sudt', `/${typeArgs}/`)) + // const sudtModel = appRegistry.findOrBind(new ActorReference('sudt', `/${typeArgs}/`)) - return SudtResponse.ok(sudtModel.getSudtBalance(addresses.map((v) => getLock(v)))) - } + // return SudtResponse.ok(sudtModel.getSudtBalance(addresses.map((v) => getLock(v)))) + // } - @Post('/send') - async send( - @Body() { from, to, amount, typeArgs }: { from: string[]; to: string; amount: HexString; typeArgs: Hash }, - ) { - if (!from?.length || !to || !amount || !typeArgs) { - throw new BadRequest('undefined body field: from, to, amount or typeArgs') + @Post('/send/:typeId') + async send(@Body() { from, to, amount }: MintRequest, @Param('typeId') typeId: string) { + if (!from?.length || !to || !amount) { + throw new BadRequest('undefined body field: from, to or amount') } - const sudtModel = appRegistry.findOrBind(new ActorReference('sudt', `/${typeArgs}/`)) - const result = sudtModel.send( - from.map((v) => getLock(v)), - getLock(to), - amount, + const token = await this._dataSource.getRepository(Token).findOneBy({ typeId }) + if (!token) { + return SudtResponse.err('404', 'token not found') + } + + const fromLocks = from.map((v) => getLock(v)) + + const sudtModel = appRegistry.findOrBind( + new ActorReference('sudt', `/${token.args}/${fromLocks[0].args}/`), + ) + const omnilockModel = appRegistry.findOrBind( + new ActorReference('omnilock', `/${fromLocks[0].args}/`), ) + + const result = sudtModel.send(omnilockModel, getLock(to), amount) return SudtResponse.ok(await Tx.toJsonString(result)) } @@ -77,7 +85,7 @@ export default class SudtController extends BaseController { return SudtResponse.ok(await Tx.toJsonString(result)) } - @Post('/token') + @Post('/') async createToken(@Body() req: CreateTokenRequest) { let owner = await this._dataSource.getRepository(Account).findOneBy({ address: req.account }) if (!owner) { @@ -119,7 +127,7 @@ export default class SudtController extends BaseController { } } - @Put('/token/:typeId') + @Put('/:typeId') async updateToken(@Body() req: CreateTokenRequest, @Param('typeId') typeId: string) { const token = await this._dataSource.getRepository(Token).findOneBy({ typeId }) if (!token) { @@ -148,7 +156,7 @@ export default class SudtController extends BaseController { } } - @Get('/token/:typeId') + @Get('/:typeId') async getToken(@Param('typeId') typeId: string) { const token = await this._dataSource.getRepository(Token).findOneBy({ typeId }) @@ -159,7 +167,7 @@ export default class SudtController extends BaseController { } } - @Get('/tokens') + @Get('/') async listTokens() { const tokens = await this._dataSource.getRepository(Token).find() diff --git a/packages/samples/sudt/src/entities/asset.entity.ts b/packages/samples/sudt/src/entities/asset.entity.ts new file mode 100644 index 00000000..a1e152bb --- /dev/null +++ b/packages/samples/sudt/src/entities/asset.entity.ts @@ -0,0 +1,32 @@ +import { BI } from '@ckb-lumos/lumos' +import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm' + +@Entity() +export class Asset { + @PrimaryGeneratedColumn() + id!: number + + @Column() + accountId!: number + + @Column() + balance!: string + + @Column() + tokenId!: number + + @Column() + typeId!: string + + @CreateDateColumn() + createdAt!: Date + + @UpdateDateColumn() + updatedAt!: Date + + toBI = () => BI.from(this.balance) + + setBalance = (balance: BI) => { + this.balance = balance.toString() + } +} diff --git a/packages/samples/sudt/src/services/explorer.service.ts b/packages/samples/sudt/src/services/explorer.service.ts index e000bcc6..3018ff17 100644 --- a/packages/samples/sudt/src/services/explorer.service.ts +++ b/packages/samples/sudt/src/services/explorer.service.ts @@ -1,5 +1,5 @@ export class ExplorerService { - constructor(private host = '') {} + constructor(private host = 'https://testnet-api.explorer.nervos.org') {} updateSUDT = async (params: { typeHash: string @@ -15,7 +15,14 @@ export class ExplorerService { email: string token?: string }) => { - const res = await fetch(`${this.host}/api/v1/udts/${params.typeHash}`, { method: 'POST', body: JSON.stringify({}) }) + const res = await fetch(`${this.host}/api/v1/udts/${params.typeHash}`, { + method: 'POST', + body: JSON.stringify({}), + headers: { + Accept: 'application/vnd.api+json', + 'Content-Type': 'application/vnd.api+json', + }, + }) if (!res.ok) { throw new Error('Internal Service Error') } diff --git a/packages/samples/sudt/src/tasks/balance.task.ts b/packages/samples/sudt/src/tasks/balance.task.ts new file mode 100644 index 00000000..d4d1a08e --- /dev/null +++ b/packages/samples/sudt/src/tasks/balance.task.ts @@ -0,0 +1,55 @@ +import { DataSource, Repository } from 'typeorm' +import { scheduler } from 'node:timers/promises' +import { Account } from '../entities/account.entity' +import { Token } from '../entities/token.entity' +import { getLock } from '../utils' +import { SudtModel, appRegistry } from '../actors' +import { ActorReference } from '@ckb-js/kuai-models' +import { Asset } from '../entities/asset.entity' + +export class BalanceTask { + #accountRepo: Repository + #tokenRepo: Repository + #assetRepo: Repository + constructor(private _dataSource: DataSource) { + this.#accountRepo = this._dataSource.getRepository(Account) + this.#tokenRepo = this._dataSource.getRepository(Token) + this.#assetRepo = this._dataSource.getRepository(Asset) + } + + run = async () => { + for (;;) { + const accounts = await this.#accountRepo.find() + const tokens = await this.#tokenRepo.find() + for (const account of accounts) { + try { + const lockscript = getLock(account.address) + for (const token of tokens) { + const sudtModel = appRegistry.findOrBind( + new ActorReference('sudt', `/${token.args}/${lockscript.args}/`), + ) + + const balance = sudtModel.getSudtBalance() + let asset = await this.#assetRepo.findOneBy({ typeId: token.typeId, accountId: account.id }) + + if (!asset) { + asset = this.#assetRepo.create({ + accountId: account.id, + tokenId: token.id, + typeId: token.typeId, + balance: balance.toString(), + }) + } else { + asset.setBalance(balance.sudtBalance) + } + + await this.#assetRepo.save(asset) + } + } catch (e) { + console.error(e) + } + } + await scheduler.wait(10000) + } + } +} From 3997f2ad6f8dca64e236361590830d119c45f1d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=83=E5=81=B6=E4=BB=80=E4=B9=88=E7=9A=84=E5=B0=B1?= =?UTF-8?q?=E6=98=AF=E5=B8=83=E5=81=B6?= Date: Thu, 30 Nov 2023 20:18:45 +0800 Subject: [PATCH 17/49] chore: fix sudt backend cors & support mysql8 (#567) --- package-lock.json | 47 +++++++++++++++++++ packages/samples/sudt/package.json | 1 + .../samples/sudt/src/actors/sudt.model.ts | 2 +- packages/samples/sudt/src/bootstrap.ts | 6 +++ .../src/controllers/account.controller.ts | 24 ++++++---- 5 files changed, 69 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index a1ca7452..f0816e77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15162,6 +15162,7 @@ "http-errors": "2.0.0", "koa": "2.14.1", "koa-body": "6.0.1", + "mysql": "npm:mysql2@3.6.1", "mysql2": "3.6.1", "typeorm": "0.3.17" }, @@ -15194,6 +15195,17 @@ "node": ">= 0.6" } }, + "packages/samples/sudt/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" + } + }, "packages/samples/sudt/node_modules/koa": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/koa/-/koa-2.14.1.tgz", @@ -15227,6 +15239,41 @@ "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" } }, + "packages/samples/sudt/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" + } + }, + "packages/samples/sudt/node_modules/mysql": { + "name": "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" + } + }, + "packages/samples/sudt/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" + } + }, "packages/samples/sudt/node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", diff --git a/packages/samples/sudt/package.json b/packages/samples/sudt/package.json index 49b64236..47d8b380 100644 --- a/packages/samples/sudt/package.json +++ b/packages/samples/sudt/package.json @@ -17,6 +17,7 @@ "http-errors": "2.0.0", "koa": "2.14.1", "koa-body": "6.0.1", + "mysql": "npm:mysql2@3.6.1", "mysql2": "3.6.1", "typeorm": "0.3.17" }, diff --git a/packages/samples/sudt/src/actors/sudt.model.ts b/packages/samples/sudt/src/actors/sudt.model.ts index 93979e4b..90b8db4e 100644 --- a/packages/samples/sudt/src/actors/sudt.model.ts +++ b/packages/samples/sudt/src/actors/sudt.model.ts @@ -91,7 +91,7 @@ export class SudtModel extends JSONStore> { return true }) if (currentTotalCapacity.lt(needCapacity)) { - inputs = inputs.concat(omnilock.loadCapacity(needCapacity.sub(currentTotalCapacity))) + inputs = inputs.concat(omnilock.loadCapacity(needCapacity.sub(currentTotalCapacity)).inputs) } if (currentTotalSudt.lt(amount)) throw new InternalServerError('not enough sudt balance') diff --git a/packages/samples/sudt/src/bootstrap.ts b/packages/samples/sudt/src/bootstrap.ts index 6cecb19d..86a9a011 100644 --- a/packages/samples/sudt/src/bootstrap.ts +++ b/packages/samples/sudt/src/bootstrap.ts @@ -1,5 +1,6 @@ import Koa from 'koa' import { koaBody } from 'koa-body' +import cors from '@koa/cors' import { getGenesisScriptsConfig, initialKuai } from '@ckb-js/kuai-core' import { KoaRouterAdapter, CoR } from '@ckb-js/kuai-io' import SudtController from './controllers/sudt.controller' @@ -34,6 +35,10 @@ const initiateDataSource = async () => { return dataSource } +process.on('uncaughtException', (error) => { + console.log(error) +}) + export const bootstrap = async () => { const kuaiCtx = await initialKuai() const kuaiEnv = kuaiCtx.getRuntimeEnvironment() @@ -80,6 +85,7 @@ export const bootstrap = async () => { const koaRouterAdapter = new KoaRouterAdapter(cor) + app.use(cors()) app.use(koaRouterAdapter.routes()) // while (true) { diff --git a/packages/samples/sudt/src/controllers/account.controller.ts b/packages/samples/sudt/src/controllers/account.controller.ts index 029f0552..6c1dbd7c 100644 --- a/packages/samples/sudt/src/controllers/account.controller.ts +++ b/packages/samples/sudt/src/controllers/account.controller.ts @@ -20,6 +20,18 @@ export class AccountController extends BaseController { super() } + async getOrCreateAccount(address: string) { + const repo = this._dataSource.getRepository(Account) + const account = await repo.findOneBy({ address }) + if (account) { + return account + } + + appRegistry.findOrBind(new ActorReference('omnilock', `/${getLock(address).args}/`)) + + return repo.save(repo.create({ address })) + } + @Post('/mint/:typeId') async mint(@Body() { from, to, amount }: MintRequest, @Param('typeId') typeId: string) { if (!from || from.length === 0 || !to || !amount) { @@ -45,12 +57,7 @@ export class AccountController extends BaseController { if (!address) { throw new Request('invalid address') } - - const repo = this._dataSource.getRepository(Account) - const account = await repo.findBy({ address }) - if (!account) { - repo.save(repo.create({ address })) - } + await this.getOrCreateAccount(address) const omniLockModel = appRegistry.findOrBind( new ActorReference('omnilock', `/${getLock(address).args}/`), @@ -82,10 +89,7 @@ export class AccountController extends BaseController { @Get('/:address/assets') async accountAssets(@Param('address') address: string) { const tokens = await this._dataSource.getRepository(Token).find() - const account = await this._dataSource.getRepository(Account).findOneBy({ address }) - if (!account) { - throw SudtResponse.err(404, 'account not found') - } + const account = await this.getOrCreateAccount(address) const assets = await this._dataSource.getRepository(Asset).findBy({ accountId: account.id }) const assetsMap = assets.reduce((acc, cur) => { From 3afa248654d8ad92cb388296713e6c7f298bed6e Mon Sep 17 00:00:00 2001 From: daryl Date: Tue, 5 Dec 2023 16:34:48 +0800 Subject: [PATCH 18/49] feat(sudt): support acp --- packages/samples/sudt/src/actors/acp.model.ts | 112 ++++++++++++++++++ .../samples/sudt/src/actors/lock.model.ts | 26 ++++ .../samples/sudt/src/actors/omnilock.model.ts | 8 +- .../samples/sudt/src/actors/sudt.model.ts | 4 +- .../src/controllers/account.controller.ts | 35 +----- .../sudt/src/controllers/sudt.controller.ts | 28 ++--- packages/samples/sudt/src/views/tx.view.ts | 13 +- 7 files changed, 170 insertions(+), 56 deletions(-) create mode 100644 packages/samples/sudt/src/actors/acp.model.ts create mode 100644 packages/samples/sudt/src/actors/lock.model.ts diff --git a/packages/samples/sudt/src/actors/acp.model.ts b/packages/samples/sudt/src/actors/acp.model.ts new file mode 100644 index 00000000..122d6402 --- /dev/null +++ b/packages/samples/sudt/src/actors/acp.model.ts @@ -0,0 +1,112 @@ +import { + ActorProvider, + ActorReference, + CellPattern, + DataFilter, + DefaultScript, + LockFilter, + OutPointString, + Param, + SchemaPattern, + UpdateStorageValue, +} from '@ckb-js/kuai-models' +import { BI, Cell, Script, utils } from '@ckb-lumos/lumos' +import { minimalCellCapacity } from '@ckb-lumos/helpers' +import { getConfig } from '@ckb-lumos/config-manager' +import { InternalServerError } from 'http-errors' +import { bytes, number } from '@ckb-lumos/codec' +import { TX_FEE } from '../constant' +import { LockModel } from './lock.model' + +@ActorProvider({ ref: { name: 'acp', path: `/:args/` } }) +@LockFilter() +@DefaultScript('ANYONE_CAN_PAY') +@DataFilter('0x') +export class ACPModel extends LockModel { + constructor( + @Param('args') args: string, + _schemaOption?: void, + params?: { + states?: Record + chainData?: Record + cellPattern?: CellPattern + schemaPattern?: SchemaPattern + }, + ) { + super(undefined, { ...params, ref: ActorReference.newWithFilter(ACPModel, `/${args}/`) }) + if (!this.lockScript) { + throw new Error('lock script is required') + } + this.registerResourceBinding() + } + + get meta(): Record<'capacity', string> { + const cells = Object.values(this.chainData).filter((v) => !v.cell.cellOutput.type) + const capacity = cells.reduce((acc, cur) => BigInt(cur.cell.cellOutput.capacity ?? 0) + acc, BigInt(0)).toString() + return { + capacity, + } + } + + loadCapacity = (capacity: BI) => { + const cells = Object.values(this.chainData) + let currentTotalCapacity = BI.from(0) + const inputs = cells.filter((v) => { + if (currentTotalCapacity.gte(capacity)) return false + currentTotalCapacity = currentTotalCapacity.add(BI.from(v.cell.cellOutput.capacity)) + return true + }) + + if (currentTotalCapacity.lt(capacity)) throw new InternalServerError('not enough capacity') + + return { inputs, currentTotalCapacity } + } + + mint = ( + lockScript: Script, + amount: BI, + args?: string, + ): { + inputs: Cell[] + outputs: Cell[] + witnesses: string[] + typeScript: Script + } => { + const CONFIG = getConfig() + const typeScript = { + codeHash: CONFIG.SCRIPTS.SUDT!.CODE_HASH, + hashType: CONFIG.SCRIPTS.SUDT!.HASH_TYPE, + args: args ?? utils.computeScriptHash(lockScript), + } + const sudtCell: Cell = { + cellOutput: { + // capacity: BI.from(MIN_SUDT_WITH_OMINILOCK).toHexString(), + capacity: '0x0', + lock: lockScript, + type: typeScript, + }, + data: bytes.hexify(number.Uint128LE.pack(amount.toHexString())), + } + sudtCell.cellOutput.capacity = `0x${minimalCellCapacity(sudtCell).toString(16)}` + // additional 0.001 ckb for tx fee + const needCapacity = BI.from(sudtCell.cellOutput.capacity).add(TX_FEE) + const { inputs, currentTotalCapacity } = this.loadCapacity(needCapacity) + + return { + typeScript, + inputs: inputs.map((v) => v.cell), + outputs: [ + sudtCell, + { + cellOutput: { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + lock: this.lockScript!, + capacity: currentTotalCapacity.sub(needCapacity).toHexString(), + }, + data: '0x', + }, + ], + witnesses: [], + } + } +} diff --git a/packages/samples/sudt/src/actors/lock.model.ts b/packages/samples/sudt/src/actors/lock.model.ts new file mode 100644 index 00000000..ba40bec6 --- /dev/null +++ b/packages/samples/sudt/src/actors/lock.model.ts @@ -0,0 +1,26 @@ +import { ActorReference, JSONStore, UpdateStorageValue } from '@ckb-js/kuai-models' +import { BI, Cell, Script } from '@ckb-lumos/lumos' +import { getConfig } from '@ckb-lumos/config-manager' +import { BadRequest } from 'http-errors' +import { appRegistry } from '.' +import { getLock } from '../utils' + +export abstract class LockModel extends JSONStore> { + static getLock = (address: string): LockModel => { + const lock = getLock(address) + if (lock.codeHash === getConfig().SCRIPTS.ANYONE_CAN_PAY?.CODE_HASH) { + return appRegistry.findOrBind(new ActorReference('acp', `/${lock.args}/`)) + } else if (lock.codeHash === getConfig().SCRIPTS.OMNILOCK?.CODE_HASH) { + return appRegistry.findOrBind(new ActorReference('omnilock', `/${lock.args}/`)) + } else { + throw new BadRequest('not support address') + } + } + abstract meta: Record<'capacity', string> + abstract loadCapacity: (capacity: BI) => { inputs: UpdateStorageValue[]; currentTotalCapacity: BI } + abstract mint: ( + lockScript: Script, + amount: BI, + args?: string, + ) => { inputs: Cell[]; outputs: Cell[]; witnesses: string[]; typeScript: Script } +} diff --git a/packages/samples/sudt/src/actors/omnilock.model.ts b/packages/samples/sudt/src/actors/omnilock.model.ts index 236b9623..00d07989 100644 --- a/packages/samples/sudt/src/actors/omnilock.model.ts +++ b/packages/samples/sudt/src/actors/omnilock.model.ts @@ -11,7 +11,6 @@ import { Param, ActorReference, CellPattern, - JSONStore, OutPointString, SchemaPattern, UpdateStorageValue, @@ -24,6 +23,7 @@ import { utils } from '@ckb-lumos/base' import { getConfig } from '@ckb-lumos/config-manager' import { InternalServerError } from 'http-errors' import { MIN_SUDT_WITH_OMINILOCK, TX_FEE } from '../const' +import { LockModel } from './lock.model' /** * add business logic in an actor @@ -32,7 +32,7 @@ import { MIN_SUDT_WITH_OMINILOCK, TX_FEE } from '../const' @LockFilter() @Omnilock() @DataFilter('0x') -export class OmnilockModel extends JSONStore> { +export class OmnilockModel extends LockModel { constructor( @Param('args') args: string, _schemaOption?: void, @@ -72,7 +72,7 @@ export class OmnilockModel extends JSONStore> { return { inputs, currentTotalCapacity } } - mint( + mint = ( lockScript: Script, amount: BI, args?: string, @@ -81,7 +81,7 @@ export class OmnilockModel extends JSONStore> { outputs: Cell[] witnesses: string[] typeScript: Script - } { + } => { const CONFIG = getConfig() const typeScript = { codeHash: CONFIG.SCRIPTS.SUDT!.CODE_HASH, diff --git a/packages/samples/sudt/src/actors/sudt.model.ts b/packages/samples/sudt/src/actors/sudt.model.ts index 90b8db4e..6cfa2e03 100644 --- a/packages/samples/sudt/src/actors/sudt.model.ts +++ b/packages/samples/sudt/src/actors/sudt.model.ts @@ -23,7 +23,7 @@ import { number, bytes } from '@ckb-lumos/codec' import { InternalServerError } from 'http-errors' import { BI, utils, config } from '@ckb-lumos/lumos' import { MIN_SUDT_WITH_OMINILOCK, TX_FEE } from '../const' -import { OmnilockModel } from './omnilock.model' +import { LockModel } from './lock.model' /** * add business logic in an actor @@ -65,7 +65,7 @@ export class SudtModel extends JSONStore> { } } - send(omnilock: OmnilockModel, lockScript: Script, amount: HexString) { + send(omnilock: LockModel, lockScript: Script, amount: HexString) { const CONFIG = config.getConfig() const sudtCell: Cell = { cellOutput: { diff --git a/packages/samples/sudt/src/controllers/account.controller.ts b/packages/samples/sudt/src/controllers/account.controller.ts index 6c1dbd7c..b66683a2 100644 --- a/packages/samples/sudt/src/controllers/account.controller.ts +++ b/packages/samples/sudt/src/controllers/account.controller.ts @@ -2,8 +2,6 @@ import { BaseController, Body, Controller, Get, Param, Post } from '@ckb-js/kuai import { DataSource } from 'typeorm' import { Transaction } from '../entities/transaction.entity' import { Account } from '../entities/account.entity' -import { OmnilockModel, appRegistry } from '../actors' -import { ActorReference } from '@ckb-js/kuai-models' import { getLock } from '../utils' import { SudtResponse } from '../response' import { Token } from '../entities/token.entity' @@ -12,6 +10,7 @@ import { Tx } from '../views/tx.view' import { MintRequest } from '../dto/mint.dto' import { BI } from '@ckb-lumos/lumos' import { Asset } from '../entities/asset.entity' +import { LockModel } from '../actors/lock.model' @Controller('/account') export class AccountController extends BaseController { @@ -27,7 +26,7 @@ export class AccountController extends BaseController { return account } - appRegistry.findOrBind(new ActorReference('omnilock', `/${getLock(address).args}/`)) + LockModel.getLock(address) return repo.save(repo.create({ address })) } @@ -43,11 +42,9 @@ export class AccountController extends BaseController { return SudtResponse.err(404, 'token not found') } - const omniLockModel = appRegistry.findOrBind( - new ActorReference('omnilock', `/${getLock(from[0]).args}/`), - ) + const lockModel = LockModel.getLock(from[0]) - const result = omniLockModel.mint(getLock(to), BI.isBI(amount) ? BI.from(amount) : BI.from(0), token.args) + const result = lockModel.mint(getLock(to), BI.isBI(amount) ? BI.from(amount) : BI.from(0), token.args) return SudtResponse.ok(await Tx.toJsonString(result)) } @@ -59,11 +56,9 @@ export class AccountController extends BaseController { } await this.getOrCreateAccount(address) - const omniLockModel = appRegistry.findOrBind( - new ActorReference('omnilock', `/${getLock(address).args}/`), - ) + const lockModel = LockModel.getLock(address) - return SudtResponse.ok(omniLockModel?.meta) + return SudtResponse.ok(lockModel?.meta) } @Get('/:address/assets/transaction') @@ -110,22 +105,4 @@ export class AccountController extends BaseController { } }) } - - // @Post('/transfer/:typeId') - // async transfer(@Param('tokenId') typeId: string, @Body() { from, to, amount }: MintRequest) { - // if (!from || from.length === 0 || !to || !amount) { - // throw new BadRequest('undefined body field: from, to or amount') - // } - - // const token = await this._dataSource.getRepository(Token).findOneBy({ typeId }) - // if (!token) { - // return SudtResponse.err(404, 'token not found') - // } - - // const sudt = appRegistry.findOrBind(new ActorReference('sudt', `/${token.args}/`)) - - // const result = sudt.send(getLock(to), amount) - - // return Tx.toJsonString(result) - // } } diff --git a/packages/samples/sudt/src/controllers/sudt.controller.ts b/packages/samples/sudt/src/controllers/sudt.controller.ts index fb2b9273..eb50e4b6 100644 --- a/packages/samples/sudt/src/controllers/sudt.controller.ts +++ b/packages/samples/sudt/src/controllers/sudt.controller.ts @@ -1,7 +1,7 @@ import type { HexString, Hash } from '@ckb-lumos/base' import { ActorReference } from '@ckb-js/kuai-models' import { BadRequest, NotFound } from 'http-errors' -import { OmnilockModel, SudtModel, appRegistry } from '../actors' +import { SudtModel, appRegistry } from '../actors' import { Tx } from '../views/tx.view' import { getLock } from '../utils' import { BaseController, Body, Controller, Get, Param, Post, Put } from '@ckb-js/kuai-io' @@ -14,6 +14,7 @@ import { tokenEntityToDto } from '../dto/token.dto' import { ExplorerService } from '../services/explorer.service' import { BI, utils } from '@ckb-lumos/lumos' import { MintRequest } from '../dto/mint.dto' +import { LockModel } from '../actors/lock.model' @Controller('token') export default class SudtController extends BaseController { @@ -36,17 +37,6 @@ export default class SudtController extends BaseController { return SudtResponse.ok(sudtModel.meta()) } - // @Post('/getSudtBalance') - // async getSudtBalance(@Body() { addresses, typeArgs }: { addresses: string[]; typeArgs: Hash }) { - // if (!addresses?.length || !typeArgs) { - // throw new BadRequest('undefined body field: from or typeArgs') - // } - - // const sudtModel = appRegistry.findOrBind(new ActorReference('sudt', `/${typeArgs}/`)) - - // return SudtResponse.ok(sudtModel.getSudtBalance(addresses.map((v) => getLock(v)))) - // } - @Post('/send/:typeId') async send(@Body() { from, to, amount }: MintRequest, @Param('typeId') typeId: string) { if (!from?.length || !to || !amount) { @@ -63,11 +53,10 @@ export default class SudtController extends BaseController { const sudtModel = appRegistry.findOrBind( new ActorReference('sudt', `/${token.args}/${fromLocks[0].args}/`), ) - const omnilockModel = appRegistry.findOrBind( - new ActorReference('omnilock', `/${fromLocks[0].args}/`), - ) - const result = sudtModel.send(omnilockModel, getLock(to), amount) + const lockModel = LockModel.getLock(from[0]) + + const result = sudtModel.send(lockModel, getLock(to), amount) return SudtResponse.ok(await Tx.toJsonString(result)) } @@ -96,10 +85,9 @@ export default class SudtController extends BaseController { const amount = BI.isBI(req.amount) ? BI.from(req.amount) : BI.from(0) try { - const omniLockModel = appRegistry.findOrBind( - new ActorReference('omnilock', `/${getLock(req.account).args}/`), - ) - const { typeScript, ...result } = omniLockModel.mint(getLock(req.account), amount) + const lockModel = LockModel.getLock(req.account) + + const { typeScript, ...result } = lockModel.mint(getLock(req.account), amount) await this._dataSource.getRepository(Token).save( this._dataSource.getRepository(Token).create({ diff --git a/packages/samples/sudt/src/views/tx.view.ts b/packages/samples/sudt/src/views/tx.view.ts index fd026b29..bf0159f7 100644 --- a/packages/samples/sudt/src/views/tx.view.ts +++ b/packages/samples/sudt/src/views/tx.view.ts @@ -1,5 +1,7 @@ import { type Cell, helpers, commons } from '@ckb-lumos/lumos' +import { getConfig } from '@ckb-lumos/config-manager' import { addBuiltInCellDeps } from '@ckb-js/kuai-common' +import { HttpError } from 'http-errors' export class Tx { static async toJsonString({ @@ -13,7 +15,16 @@ export class Tx { }): Promise { let txSkeleton = helpers.TransactionSkeleton({}) for (const input of inputs) { - txSkeleton = await commons.omnilock.setupInputCell(txSkeleton, input) + switch (input.cellOutput.lock.args) { + case getConfig().SCRIPTS.ANYONE_CAN_PAY?.CODE_HASH: + txSkeleton = await commons.anyoneCanPay.setupInputCell(txSkeleton, input) + break + case getConfig().SCRIPTS.OMNILOCK?.CODE_HASH: + txSkeleton = await commons.omnilock.setupInputCell(txSkeleton, input) + break + default: + throw new HttpError('not support lock script') + } txSkeleton = txSkeleton.remove('outputs') } txSkeleton = txSkeleton.update('outputs', (v) => v.push(...outputs)) From 2684afeb7c2e23628cf32896aeeb77307a719ec8 Mon Sep 17 00:00:00 2001 From: daryl Date: Wed, 6 Dec 2023 14:29:26 +0800 Subject: [PATCH 19/49] fix(sudt): wrong class used --- packages/samples/sudt/src/actors/lock.model.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/samples/sudt/src/actors/lock.model.ts b/packages/samples/sudt/src/actors/lock.model.ts index ba40bec6..c3cadf7c 100644 --- a/packages/samples/sudt/src/actors/lock.model.ts +++ b/packages/samples/sudt/src/actors/lock.model.ts @@ -2,16 +2,17 @@ import { ActorReference, JSONStore, UpdateStorageValue } from '@ckb-js/kuai-mode import { BI, Cell, Script } from '@ckb-lumos/lumos' import { getConfig } from '@ckb-lumos/config-manager' import { BadRequest } from 'http-errors' -import { appRegistry } from '.' +import { OmnilockModel, appRegistry } from '.' import { getLock } from '../utils' +import { ACPModel } from './acp.model' export abstract class LockModel extends JSONStore> { static getLock = (address: string): LockModel => { const lock = getLock(address) if (lock.codeHash === getConfig().SCRIPTS.ANYONE_CAN_PAY?.CODE_HASH) { - return appRegistry.findOrBind(new ActorReference('acp', `/${lock.args}/`)) + return appRegistry.findOrBind(new ActorReference('acp', `/${lock.args}/`)) } else if (lock.codeHash === getConfig().SCRIPTS.OMNILOCK?.CODE_HASH) { - return appRegistry.findOrBind(new ActorReference('omnilock', `/${lock.args}/`)) + return appRegistry.findOrBind(new ActorReference('omnilock', `/${lock.args}/`)) } else { throw new BadRequest('not support address') } From 6a3b429cb4a10ea8236d858afe75e2c195b801ee Mon Sep 17 00:00:00 2001 From: daryl Date: Thu, 7 Dec 2023 14:34:44 +0800 Subject: [PATCH 20/49] fix(sudt): async --- packages/samples/sudt/src/controllers/sudt.controller.ts | 2 +- packages/samples/sudt/src/views/tx.view.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/samples/sudt/src/controllers/sudt.controller.ts b/packages/samples/sudt/src/controllers/sudt.controller.ts index eb50e4b6..80efc1b7 100644 --- a/packages/samples/sudt/src/controllers/sudt.controller.ts +++ b/packages/samples/sudt/src/controllers/sudt.controller.ts @@ -101,7 +101,7 @@ export default class SudtController extends BaseController { typeId: utils.computeScriptHash(typeScript), }), ) - return new SudtResponse('201', Tx.toJsonString(result)) + return new SudtResponse('201', await Tx.toJsonString(result)) } catch (e) { if (e instanceof QueryFailedError) { switch (e.driverError.code) { diff --git a/packages/samples/sudt/src/views/tx.view.ts b/packages/samples/sudt/src/views/tx.view.ts index bf0159f7..4d4c559d 100644 --- a/packages/samples/sudt/src/views/tx.view.ts +++ b/packages/samples/sudt/src/views/tx.view.ts @@ -1,7 +1,7 @@ import { type Cell, helpers, commons } from '@ckb-lumos/lumos' import { getConfig } from '@ckb-lumos/config-manager' import { addBuiltInCellDeps } from '@ckb-js/kuai-common' -import { HttpError } from 'http-errors' +import { SudtResponse } from '../response' export class Tx { static async toJsonString({ @@ -15,7 +15,7 @@ export class Tx { }): Promise { let txSkeleton = helpers.TransactionSkeleton({}) for (const input of inputs) { - switch (input.cellOutput.lock.args) { + switch (input.cellOutput.lock.codeHash) { case getConfig().SCRIPTS.ANYONE_CAN_PAY?.CODE_HASH: txSkeleton = await commons.anyoneCanPay.setupInputCell(txSkeleton, input) break @@ -23,7 +23,7 @@ export class Tx { txSkeleton = await commons.omnilock.setupInputCell(txSkeleton, input) break default: - throw new HttpError('not support lock script') + throw SudtResponse.err('400', 'not support lock script') } txSkeleton = txSkeleton.remove('outputs') } From 4d31450f8ed855b71e31951b6a2cd1581f9042dc Mon Sep 17 00:00:00 2001 From: daryl Date: Wed, 13 Dec 2023 19:15:46 +0800 Subject: [PATCH 21/49] feat(sudt): transfer history --- packages/samples/sudt/src/bootstrap.ts | 4 +- .../src/controllers/account.controller.ts | 41 +++++---- .../sudt/src/services/nervos.service.ts | 86 +++++++++++++++++++ 3 files changed, 112 insertions(+), 19 deletions(-) create mode 100644 packages/samples/sudt/src/services/nervos.service.ts diff --git a/packages/samples/sudt/src/bootstrap.ts b/packages/samples/sudt/src/bootstrap.ts index 86a9a011..3dbea74b 100644 --- a/packages/samples/sudt/src/bootstrap.ts +++ b/packages/samples/sudt/src/bootstrap.ts @@ -16,6 +16,7 @@ import { DataSource } from 'typeorm' import { AccountController } from './controllers/account.controller' import { ExplorerService } from './services/explorer.service' import { BalanceTask } from './tasks/balance.task' +import { NervosService } from './services/nervos.service' const initiateDataSource = async () => { const dataSource = new DataSource({ @@ -75,11 +76,12 @@ export const bootstrap = async () => { const balanceTask = new BalanceTask(dataSource) balanceTask.run() + const nervosService = new NervosService(kuaiEnv.config.ckbChain.rpcUrl, kuaiEnv.config.ckbChain.rpcUrl) // init kuai io const cor = new CoR() const sudtController = new SudtController(dataSource, new ExplorerService(process.env.EXPLORER_API_HOST)) - const accountController = new AccountController(dataSource) + const accountController = new AccountController(dataSource, nervosService) cor.use(sudtController.middleware()) cor.use(accountController.middleware()) diff --git a/packages/samples/sudt/src/controllers/account.controller.ts b/packages/samples/sudt/src/controllers/account.controller.ts index b66683a2..18a82fd7 100644 --- a/packages/samples/sudt/src/controllers/account.controller.ts +++ b/packages/samples/sudt/src/controllers/account.controller.ts @@ -1,6 +1,5 @@ -import { BaseController, Body, Controller, Get, Param, Post } from '@ckb-js/kuai-io' +import { BaseController, Body, Controller, Get, Param, Post, Query } from '@ckb-js/kuai-io' import { DataSource } from 'typeorm' -import { Transaction } from '../entities/transaction.entity' import { Account } from '../entities/account.entity' import { getLock } from '../utils' import { SudtResponse } from '../response' @@ -9,13 +8,18 @@ import { BadRequest } from 'http-errors' import { Tx } from '../views/tx.view' import { MintRequest } from '../dto/mint.dto' import { BI } from '@ckb-lumos/lumos' +import { getConfig } from '@ckb-lumos/config-manager' import { Asset } from '../entities/asset.entity' import { LockModel } from '../actors/lock.model' +import { NervosService } from '../services/nervos.service' @Controller('/account') export class AccountController extends BaseController { #explorerHost = process.env.EXPLORER_HOST || 'https://explorer.nervos.org' - constructor(private _dataSource: DataSource) { + constructor( + private _dataSource: DataSource, + private _nervosService: NervosService, + ) { super() } @@ -61,24 +65,25 @@ export class AccountController extends BaseController { return SudtResponse.ok(lockModel?.meta) } - @Get('/:address/assets/transaction') - async accountTransaction(@Param('address') address: string) { - const account = await this._dataSource.getRepository(Account).findOneBy({ address }) - if (!account) { + @Get('/:address/assets/transaction/:typeId') + async accountTransaction( + @Param('address') address: string, + @Param('typeId') typeId: string, + @Query('size') size: number, + @Query('lastCursor') lastCursor?: string, + ) { + const token = await this._dataSource.getRepository(Token).findOneBy({ typeId }) + if (!token) { return [] } - const txs = await this._dataSource.getRepository(Transaction).findBy({ fromAccountId: account.id }) - return txs.map((tx) => ({ - txHash: tx.txHash, - from: tx.fromAccountId, - to: tx.toAccountId, - time: tx.createdAt, - status: tx.status, - sudtAmount: tx.sudtAmount, - ckbAmount: tx.ckbAmount, - url: `${this.#explorerHost}/transaction/${tx.txHash}`, - })) + const typeScript = { + codeHash: getConfig().SCRIPTS.SUDT!.CODE_HASH, + hashType: getConfig().SCRIPTS.SUDT!.HASH_TYPE, + args: token.args, + } + + return this._nervosService.fetchTransferHistory(getLock(address), typeScript, size, lastCursor) } @Get('/:address/assets') diff --git a/packages/samples/sudt/src/services/nervos.service.ts b/packages/samples/sudt/src/services/nervos.service.ts new file mode 100644 index 00000000..203b8dad --- /dev/null +++ b/packages/samples/sudt/src/services/nervos.service.ts @@ -0,0 +1,86 @@ +import { BI, Indexer, Output, RPC, Script, Transaction, utils } from '@ckb-lumos/lumos' + +export class NervosService { + #indexer: Indexer + #rpc: RPC + constructor(rpcUrl: string, indexerUrl: string) { + this.#indexer = new Indexer(rpcUrl, indexerUrl) + this.#rpc = new RPC(rpcUrl) + } + + #collectTokenAmount = ( + map: Map, + typeId: string, + output: Output, + outputData: string, + lockMap: Map, + ): Map => { + if (output.type) { + if (utils.computeScriptHash(output.type) !== typeId) return map + + const lockHash = utils.computeScriptHash(output.lock) + lockMap.set(lockHash, output.lock) + + const totalAmount = map.get(typeId) ?? BI.from(0) + map.set(lockHash, totalAmount.add(BI.from(outputData))) + } + + return map + } + + #filterFrom = async (tx: Transaction, typeIds: string, lockMap: Map): Promise> => { + let from = new Map() + for (const input of tx.inputs) { + const previousTransaction = await this.#rpc.getTransaction(input.previousOutput.txHash) + const txIndex = parseInt(input.previousOutput.index, 16) + const previousOutput = previousTransaction.transaction.outputs[txIndex] + console.log(previousOutput) + const previousOutputData = previousTransaction.transaction.outputsData[txIndex] + from = this.#collectTokenAmount(from, typeIds, previousOutput, previousOutputData, lockMap) + } + + return from + } + + #filterTo = async (tx: Transaction, typeId: string, lockMap: Map): Promise> => + tx.outputs.reduce((acc, cur, key) => { + this.#collectTokenAmount(acc, typeId, cur, tx.outputsData[key], lockMap) + return acc + }, new Map()) + + fetchTransferHistory = async (lockScript: Script, typeScript: Script, sizeLimit: number, lastCursor?: string) => { + const txs = await this.#indexer.getTransactions( + { + script: lockScript, + scriptType: 'lock', + filter: { script: typeScript }, + }, + { order: 'desc', sizeLimit, lastCursor }, + ) + const lockMap = new Map() + + const history = await Promise.all( + txs.objects.map(async ({ txHash }) => { + const { transaction } = await this.#rpc.getTransaction(txHash) + const from = await this.#filterFrom(transaction, utils.computeScriptHash(typeScript), lockMap) + const to = await this.#filterTo(transaction, utils.computeScriptHash(typeScript), lockMap) + + return { + froms: Array.from(from.entries()).map(([lockHash, amount]) => ({ + lock: lockMap.get(lockHash), + amount: amount.toString(), + })), + to: Array.from(to.entries()).map(([lockHash, amount]) => ({ + lock: lockMap.get(lockHash), + amount: amount.toString(), + })), + } + }), + ) + + return { + lastCursor: txs.lastCursor, + history, + } + } +} From 5656b9798d43f9e63a9d13427e3f2f4556616a65 Mon Sep 17 00:00:00 2001 From: daryl Date: Thu, 14 Dec 2023 11:13:09 +0800 Subject: [PATCH 22/49] feat(sudt): history respond the address in `from` and `to` --- .../src/controllers/account.controller.ts | 22 +++++++++++++++++-- .../sudt/src/services/nervos.service.ts | 4 ++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/samples/sudt/src/controllers/account.controller.ts b/packages/samples/sudt/src/controllers/account.controller.ts index 18a82fd7..396e2d9d 100644 --- a/packages/samples/sudt/src/controllers/account.controller.ts +++ b/packages/samples/sudt/src/controllers/account.controller.ts @@ -8,6 +8,7 @@ import { BadRequest } from 'http-errors' import { Tx } from '../views/tx.view' import { MintRequest } from '../dto/mint.dto' import { BI } from '@ckb-lumos/lumos' +import { encodeToAddress } from '@ckb-lumos/helpers' import { getConfig } from '@ckb-lumos/config-manager' import { Asset } from '../entities/asset.entity' import { LockModel } from '../actors/lock.model' @@ -83,7 +84,25 @@ export class AccountController extends BaseController { args: token.args, } - return this._nervosService.fetchTransferHistory(getLock(address), typeScript, size, lastCursor) + const history = await this._nervosService.fetchTransferHistory(getLock(address), typeScript, size, lastCursor) + + return { + ...history, + ...{ + history: history.history.map((tx) => { + return { + froms: tx.froms.map((from) => ({ + amount: from.amount, + address: encodeToAddress(from.lock), + })), + to: tx.to.map((to) => ({ + amount: to.amount, + address: encodeToAddress(to.lock), + })), + } + }), + }, + } } @Get('/:address/assets') @@ -96,7 +115,6 @@ export class AccountController extends BaseController { acc.set(cur.tokenId, cur) return acc }, new Map()) - console.log(assetsMap) return tokens.map((token) => { try { return { diff --git a/packages/samples/sudt/src/services/nervos.service.ts b/packages/samples/sudt/src/services/nervos.service.ts index 203b8dad..d40855f0 100644 --- a/packages/samples/sudt/src/services/nervos.service.ts +++ b/packages/samples/sudt/src/services/nervos.service.ts @@ -67,11 +67,11 @@ export class NervosService { return { froms: Array.from(from.entries()).map(([lockHash, amount]) => ({ - lock: lockMap.get(lockHash), + lock: lockMap.get(lockHash)!, amount: amount.toString(), })), to: Array.from(to.entries()).map(([lockHash, amount]) => ({ - lock: lockMap.get(lockHash), + lock: lockMap.get(lockHash)!, amount: amount.toString(), })), } From a7f34f9a522753354c32314d0ebdd130a4a89521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=83=E5=81=B6=E4=BB=80=E4=B9=88=E7=9A=84=E5=B0=B1?= =?UTF-8?q?=E6=98=AF=E5=B8=83=E5=81=B6?= Date: Thu, 14 Dec 2023 11:55:57 +0800 Subject: [PATCH 23/49] chore: change mint api path (#574) --- .../src/controllers/account.controller.ts | 27 +----- .../sudt/src/controllers/sudt.controller.ts | 89 +++++++++++++------ packages/samples/sudt/src/dto/mint.dto.ts | 4 +- packages/samples/sudt/src/dto/token.dto.ts | 2 + packages/samples/sudt/src/dto/transfer.dto.ts | 12 --- 5 files changed, 68 insertions(+), 66 deletions(-) delete mode 100644 packages/samples/sudt/src/dto/transfer.dto.ts diff --git a/packages/samples/sudt/src/controllers/account.controller.ts b/packages/samples/sudt/src/controllers/account.controller.ts index 396e2d9d..70f12192 100644 --- a/packages/samples/sudt/src/controllers/account.controller.ts +++ b/packages/samples/sudt/src/controllers/account.controller.ts @@ -1,13 +1,9 @@ -import { BaseController, Body, Controller, Get, Param, Post, Query } from '@ckb-js/kuai-io' +import { BaseController, Controller, Get, Param, Query, } from '@ckb-js/kuai-io' import { DataSource } from 'typeorm' import { Account } from '../entities/account.entity' -import { getLock } from '../utils' import { SudtResponse } from '../response' import { Token } from '../entities/token.entity' -import { BadRequest } from 'http-errors' -import { Tx } from '../views/tx.view' -import { MintRequest } from '../dto/mint.dto' -import { BI } from '@ckb-lumos/lumos' +import { getLock } from '../utils' import { encodeToAddress } from '@ckb-lumos/helpers' import { getConfig } from '@ckb-lumos/config-manager' import { Asset } from '../entities/asset.entity' @@ -36,24 +32,6 @@ export class AccountController extends BaseController { return repo.save(repo.create({ address })) } - @Post('/mint/:typeId') - async mint(@Body() { from, to, amount }: MintRequest, @Param('typeId') typeId: string) { - if (!from || from.length === 0 || !to || !amount) { - throw new BadRequest('undefined body field: from, to or amount') - } - - const token = await this._dataSource.getRepository(Token).findOneBy({ typeId }) - if (!token) { - return SudtResponse.err(404, 'token not found') - } - - const lockModel = LockModel.getLock(from[0]) - - const result = lockModel.mint(getLock(to), BI.isBI(amount) ? BI.from(amount) : BI.from(0), token.args) - - return SudtResponse.ok(await Tx.toJsonString(result)) - } - @Get('/meta/:address') async meta(@Param('address') address: string) { if (!address) { @@ -122,6 +100,7 @@ export class AccountController extends BaseController { displayName: token.name, decimal: token.decimal, amount: assetsMap.get(token.id)?.balance ?? '0', + typeId: token.typeId, } } catch (e) { console.error(e) diff --git a/packages/samples/sudt/src/controllers/sudt.controller.ts b/packages/samples/sudt/src/controllers/sudt.controller.ts index 80efc1b7..09c22b43 100644 --- a/packages/samples/sudt/src/controllers/sudt.controller.ts +++ b/packages/samples/sudt/src/controllers/sudt.controller.ts @@ -13,7 +13,7 @@ import { Account } from '../entities/account.entity' import { tokenEntityToDto } from '../dto/token.dto' import { ExplorerService } from '../services/explorer.service' import { BI, utils } from '@ckb-lumos/lumos' -import { MintRequest } from '../dto/mint.dto' +import { MintRequest, TransferRequest } from '../dto/mint.dto' import { LockModel } from '../actors/lock.model' @Controller('token') @@ -38,8 +38,8 @@ export default class SudtController extends BaseController { } @Post('/send/:typeId') - async send(@Body() { from, to, amount }: MintRequest, @Param('typeId') typeId: string) { - if (!from?.length || !to || !amount) { + async send(@Body() { from, to, amount }: TransferRequest, @Param('typeId') typeId: string) { + if (!from || !to || !amount) { throw new BadRequest('undefined body field: from, to or amount') } @@ -60,6 +60,29 @@ export default class SudtController extends BaseController { return SudtResponse.ok(await Tx.toJsonString(result)) } + @Post('/mint/:typeId') + async mint(@Body() { to, amount }: MintRequest, @Param('typeId') typeId: string) { + if (!to || !amount) { + throw new BadRequest('undefined body field: from, to or amount') + } + + const token = await this._dataSource.getRepository(Token).findOneBy({ typeId }) + if (!token) { + return SudtResponse.err('404', 'token not found') + } + + const owner = await this._dataSource.getRepository(Account).findOneBy({ id: token.ownerId }) + if (!owner) { + return SudtResponse.err('404', 'token owner not found') + } + + const lockModel = LockModel.getLock(owner.address) + + const result = lockModel.mint(getLock(to), BI.isBI(amount) ? amount : BI.from(amount), token.args) + + return SudtResponse.ok(await Tx.toJsonString(result)) + } + @Post('/destory') async destory(@Body() { from, amount, typeArgs }: { from: string[]; amount: HexString; typeArgs: Hash }) { if (!from) { @@ -88,19 +111,26 @@ export default class SudtController extends BaseController { const lockModel = LockModel.getLock(req.account) const { typeScript, ...result } = lockModel.mint(getLock(req.account), amount) + const getOrCreateToken = async () => { + const checkToken = await this._dataSource.getRepository(Token).findOneBy({ name: req.name }) + if (checkToken) { + return checkToken + } + return this._dataSource.getRepository(Token).save( + this._dataSource.getRepository(Token).create({ + name: req.name, + ownerId: owner!.id, + decimal: req.decimal, + description: req.description, + website: req.website, + icon: req.icon, + args: typeScript.args, + typeId: utils.computeScriptHash(typeScript), + }), + ) + } + await getOrCreateToken() - await this._dataSource.getRepository(Token).save( - this._dataSource.getRepository(Token).create({ - name: req.name, - ownerId: owner.id, - decimal: req.decimal, - description: req.description, - website: req.website, - icon: req.icon, - args: typeScript.args, - typeId: utils.computeScriptHash(typeScript), - }), - ) return new SudtResponse('201', await Tx.toJsonString(result)) } catch (e) { if (e instanceof QueryFailedError) { @@ -122,21 +152,22 @@ export default class SudtController extends BaseController { return SudtResponse.err('404', { message: 'Token not found' }) } + this._explorerService.updateSUDT({ + typeHash: typeId, + symbol: req.name, + fullName: req.name, + decimal: req.decimal.toString(), + totalAmount: '0', + description: req.description, + operatorWebsite: req.website, + iconFile: req.icon, + uan: `${req.name}.ckb`, + displayName: req.name, + email: req.email, + token: req.explorerCode, + }) + try { - await this._explorerService.updateSUDT({ - typeHash: typeId, - symbol: req.name, - fullName: req.name, - decimal: req.decimal.toString(), - totalAmount: '0', - description: req.description, - operatorWebsite: req.website, - iconFile: req.icon, - uan: `${req.name}.ckb`, - displayName: req.name, - email: req.email, - token: req.explorerCode, - }) await this._dataSource.getRepository(Token).save({ ...token, ...req }) return new SudtResponse('201', {}) } catch (e) { diff --git a/packages/samples/sudt/src/dto/mint.dto.ts b/packages/samples/sudt/src/dto/mint.dto.ts index 6fb98ee9..6090b033 100644 --- a/packages/samples/sudt/src/dto/mint.dto.ts +++ b/packages/samples/sudt/src/dto/mint.dto.ts @@ -1,12 +1,14 @@ import { TransactionSkeletonType } from '@ckb-lumos/helpers' import { HexString } from '@ckb-lumos/lumos' -export interface MintRequest { +export interface TransferRequest { from: string[] to: string amount: HexString } +export interface MintRequest extends Omit {} + export interface MintResponse { txSkeleton: TransactionSkeletonType } diff --git a/packages/samples/sudt/src/dto/token.dto.ts b/packages/samples/sudt/src/dto/token.dto.ts index 0c00ad2e..5084fe70 100644 --- a/packages/samples/sudt/src/dto/token.dto.ts +++ b/packages/samples/sudt/src/dto/token.dto.ts @@ -2,6 +2,7 @@ import { Token } from '../entities/token.entity' export interface TokenResponse { symbol: string + typeId: string name: string amount: string decimal: number @@ -20,6 +21,7 @@ export const tokenEntityToDto = (token: Token, amount: string, explorerHost: str description: token.description ?? '', website: token.website, icon: token.icon, + typeId: token.typeId, explorerUrl: `${explorerHost}/sudt/${token.typeId}`, } } diff --git a/packages/samples/sudt/src/dto/transfer.dto.ts b/packages/samples/sudt/src/dto/transfer.dto.ts deleted file mode 100644 index b975519a..00000000 --- a/packages/samples/sudt/src/dto/transfer.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { TransactionSkeletonObject } from '@ckb-lumos/helpers' - -export interface TransferRequest { - from: string[] - to: string - amount: bigint - typeId: string -} - -export interface TransferResponse { - skeleton: TransactionSkeletonObject -} From 6d87827d675753ddce087c3d742e71165a488ceb Mon Sep 17 00:00:00 2001 From: daryl Date: Tue, 19 Dec 2023 02:01:18 +0800 Subject: [PATCH 24/49] feat(sudt): workspace --- packages/samples/sudt/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/samples/sudt/package.json b/packages/samples/sudt/package.json index 47d8b380..9d546414 100644 --- a/packages/samples/sudt/package.json +++ b/packages/samples/sudt/package.json @@ -1,5 +1,7 @@ { "name": "sudt", + "private": true, + "version": "0.0.1-alpha.2", "scripts": { "dev": "ts-node src/main.ts", "build": "tsc", From 701c5da515badc17822d1ef54ec16402e3a8b64b Mon Sep 17 00:00:00 2001 From: daryl Date: Tue, 19 Dec 2023 15:21:01 +0800 Subject: [PATCH 25/49] fix(sudt): redis config --- packages/samples/sudt/kuai.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/samples/sudt/kuai.config.ts b/packages/samples/sudt/kuai.config.ts index 88f662de..61616a42 100644 --- a/packages/samples/sudt/kuai.config.ts +++ b/packages/samples/sudt/kuai.config.ts @@ -17,8 +17,8 @@ const redisAuth = REDIS_USER && REDIS_PASSWORD ? { username: REDIS_USER, passwor const config = { port: 3000, - redisPort: REDIS_HOST ? +REDIS_HOST : undefined, - redisHost: REDIS_PORT, + redisPort: REDIS_PORT ? +REDIS_PORT : undefined, + redisHost: REDIS_HOST, network: process.env.NETWORK || 'testnet', redisOpt: redisOpt || redisAuth From f4d93f95d876dec8834c49e1d6e294736f4402d2 Mon Sep 17 00:00:00 2001 From: daryl Date: Tue, 19 Dec 2023 16:16:22 +0800 Subject: [PATCH 26/49] test(sudt): only for test --- packages/samples/sudt/kuai.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/samples/sudt/kuai.config.ts b/packages/samples/sudt/kuai.config.ts index 61616a42..b1876942 100644 --- a/packages/samples/sudt/kuai.config.ts +++ b/packages/samples/sudt/kuai.config.ts @@ -29,4 +29,6 @@ const config = { : undefined, } +console.log(config) + export default config From b73e133f3168f266422337966ef98fcf919c680c Mon Sep 17 00:00:00 2001 From: daryl Date: Tue, 19 Dec 2023 16:20:38 +0800 Subject: [PATCH 27/49] test(sudt): test for railway --- packages/samples/sudt/kuai.config.ts | 2 -- packages/samples/sudt/src/bootstrap.ts | 7 ++++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/samples/sudt/kuai.config.ts b/packages/samples/sudt/kuai.config.ts index b1876942..61616a42 100644 --- a/packages/samples/sudt/kuai.config.ts +++ b/packages/samples/sudt/kuai.config.ts @@ -29,6 +29,4 @@ const config = { : undefined, } -console.log(config) - export default config diff --git a/packages/samples/sudt/src/bootstrap.ts b/packages/samples/sudt/src/bootstrap.ts index 3dbea74b..69a16086 100644 --- a/packages/samples/sudt/src/bootstrap.ts +++ b/packages/samples/sudt/src/bootstrap.ts @@ -15,7 +15,7 @@ import { config } from '@ckb-lumos/lumos' import { DataSource } from 'typeorm' import { AccountController } from './controllers/account.controller' import { ExplorerService } from './services/explorer.service' -import { BalanceTask } from './tasks/balance.task' +// import { BalanceTask } from './tasks/balance.task' import { NervosService } from './services/nervos.service' const initiateDataSource = async () => { @@ -43,6 +43,7 @@ process.on('uncaughtException', (error) => { export const bootstrap = async () => { const kuaiCtx = await initialKuai() const kuaiEnv = kuaiCtx.getRuntimeEnvironment() + console.log(kuaiEnv.config) if (kuaiEnv.config.redisPort) { mqContainer.bind(REDIS_PORT_SYMBOL).toConstantValue(kuaiEnv.config.redisPort) @@ -74,8 +75,8 @@ export const bootstrap = async () => { const dataSource = await initiateDataSource() - const balanceTask = new BalanceTask(dataSource) - balanceTask.run() + // const balanceTask = new BalanceTask(dataSource) + // balanceTask.run() const nervosService = new NervosService(kuaiEnv.config.ckbChain.rpcUrl, kuaiEnv.config.ckbChain.rpcUrl) // init kuai io From d9c5ffe855ee17a1c82d952593b36073672944ad Mon Sep 17 00:00:00 2001 From: daryl Date: Tue, 19 Dec 2023 16:26:26 +0800 Subject: [PATCH 28/49] feat(sudt): without _ in env --- packages/samples/sudt/src/bootstrap.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/samples/sudt/src/bootstrap.ts b/packages/samples/sudt/src/bootstrap.ts index 69a16086..2b7b9ce0 100644 --- a/packages/samples/sudt/src/bootstrap.ts +++ b/packages/samples/sudt/src/bootstrap.ts @@ -22,11 +22,11 @@ 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', + host: process.env.DBHOST || 'localhost', + port: Number(process.env.DBPORT) || 3306, + username: process.env.DBUSERNAME || 'root', + password: process.env.DBPASSWORD || 'root', + database: process.env.DBDATABASE || 'sudt', entities: [__dirname + '/entities/*.{js,ts}'], synchronize: true, }) From 6c0d01196dcc8392e86c7a1a71b73c3f9ce1be33 Mon Sep 17 00:00:00 2001 From: daryl Date: Wed, 20 Dec 2023 01:26:40 +0800 Subject: [PATCH 29/49] feat(sudt): fetch all transaction --- .../src/controllers/account.controller.ts | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/packages/samples/sudt/src/controllers/account.controller.ts b/packages/samples/sudt/src/controllers/account.controller.ts index 70f12192..61c605d3 100644 --- a/packages/samples/sudt/src/controllers/account.controller.ts +++ b/packages/samples/sudt/src/controllers/account.controller.ts @@ -1,11 +1,10 @@ -import { BaseController, Controller, Get, Param, Query, } from '@ckb-js/kuai-io' +import { BaseController, Controller, Get, Param, Query } from '@ckb-js/kuai-io' import { DataSource } from 'typeorm' import { Account } from '../entities/account.entity' import { SudtResponse } from '../response' import { Token } from '../entities/token.entity' import { getLock } from '../utils' import { encodeToAddress } from '@ckb-lumos/helpers' -import { getConfig } from '@ckb-lumos/config-manager' import { Asset } from '../entities/asset.entity' import { LockModel } from '../actors/lock.model' import { NervosService } from '../services/nervos.service' @@ -44,41 +43,45 @@ export class AccountController extends BaseController { return SudtResponse.ok(lockModel?.meta) } - @Get('/:address/assets/transaction/:typeId') + @Get('/:address/assets/transaction') async accountTransaction( @Param('address') address: string, - @Param('typeId') typeId: string, @Query('size') size: number, @Query('lastCursor') lastCursor?: string, ) { - const token = await this._dataSource.getRepository(Token).findOneBy({ typeId }) - if (!token) { - return [] - } + const tokens = await this._dataSource.getRepository(Token).find() - const typeScript = { - codeHash: getConfig().SCRIPTS.SUDT!.CODE_HASH, - hashType: getConfig().SCRIPTS.SUDT!.HASH_TYPE, - args: token.args, + if (tokens.length === 0) { + return { lastCursor: '', history: [] } } - const history = await this._nervosService.fetchTransferHistory(getLock(address), typeScript, size, lastCursor) + const history = await this._nervosService.fetchTransferHistory( + getLock(address), + tokens.map((token) => token.typeId), + size, + lastCursor, + ) + + console.log(history) return { ...history, ...{ - history: history.history.map((tx) => { - return { - froms: tx.froms.map((from) => ({ - amount: from.amount, - address: encodeToAddress(from.lock), - })), - to: tx.to.map((to) => ({ - amount: to.amount, - address: encodeToAddress(to.lock), + history: history.history.map((tx) => ({ + ...tx, + ...{ + list: tx.list.map((item) => ({ + from: item.from.map((from) => ({ + amount: from.amount, + address: encodeToAddress(from.lock), + })), + to: item.to.map((to) => ({ + amount: to.amount, + address: encodeToAddress(to.lock), + })), })), - } - }), + }, + })), }, } } From 1bdd14e21f1d4d4b3de411c9ab68b8ea181cfdb8 Mon Sep 17 00:00:00 2001 From: daryl Date: Wed, 20 Dec 2023 01:38:36 +0800 Subject: [PATCH 30/49] feat(sudt): transactions --- .../sudt/src/services/nervos.service.ts | 90 +++++++++++++------ 1 file changed, 62 insertions(+), 28 deletions(-) diff --git a/packages/samples/sudt/src/services/nervos.service.ts b/packages/samples/sudt/src/services/nervos.service.ts index d40855f0..f977c498 100644 --- a/packages/samples/sudt/src/services/nervos.service.ts +++ b/packages/samples/sudt/src/services/nervos.service.ts @@ -1,5 +1,8 @@ import { BI, Indexer, Output, RPC, Script, Transaction, utils } from '@ckb-lumos/lumos' +type TypeHash = string +type LockHash = string + export class NervosService { #indexer: Indexer #rpc: RPC @@ -9,7 +12,7 @@ export class NervosService { } #collectTokenAmount = ( - map: Map, + map: Map, typeId: string, output: Output, outputData: string, @@ -19,7 +22,7 @@ export class NervosService { if (utils.computeScriptHash(output.type) !== typeId) return map const lockHash = utils.computeScriptHash(output.lock) - lockMap.set(lockHash, output.lock) + lockMap.set(lockHash, output.type) const totalAmount = map.get(typeId) ?? BI.from(0) map.set(lockHash, totalAmount.add(BI.from(outputData))) @@ -28,32 +31,52 @@ export class NervosService { return map } - #filterFrom = async (tx: Transaction, typeIds: string, lockMap: Map): Promise> => { - let from = new Map() - for (const input of tx.inputs) { - const previousTransaction = await this.#rpc.getTransaction(input.previousOutput.txHash) - const txIndex = parseInt(input.previousOutput.index, 16) - const previousOutput = previousTransaction.transaction.outputs[txIndex] - console.log(previousOutput) - const previousOutputData = previousTransaction.transaction.outputsData[txIndex] - from = this.#collectTokenAmount(from, typeIds, previousOutput, previousOutputData, lockMap) - } + #filterFrom = async ( + tx: Transaction, + typeIds: string[], + lockMap: Map, + ): Promise>> => { + const from = new Map>() + for (const typeId of typeIds) { + let fromSingleTx = new Map() + for (const input of tx.inputs) { + const previousTransaction = await this.#rpc.getTransaction(input.previousOutput.txHash) + const txIndex = parseInt(input.previousOutput.index, 16) + const previousOutput = previousTransaction.transaction.outputs[txIndex] + const previousOutputData = previousTransaction.transaction.outputsData[txIndex] + fromSingleTx = this.#collectTokenAmount(fromSingleTx, typeId, previousOutput, previousOutputData, lockMap) + } + if (Array.from(fromSingleTx.entries()).length > 0) { + from.set(typeId, fromSingleTx) + } + } return from } - #filterTo = async (tx: Transaction, typeId: string, lockMap: Map): Promise> => - tx.outputs.reduce((acc, cur, key) => { - this.#collectTokenAmount(acc, typeId, cur, tx.outputsData[key], lockMap) + #filterTo = async ( + tx: Transaction, + typeIds: string[], + lockMap: Map, + ): Promise>> => + typeIds.reduce((acc, typeId) => { + const toSingleTx = tx.outputs.reduce((acc, cur, key) => { + this.#collectTokenAmount(acc, typeId, cur, tx.outputsData[key], lockMap) + return acc + }, new Map()) + + if (Array.from(toSingleTx.entries()).length > 0) { + acc.set(typeId, toSingleTx) + } + return acc - }, new Map()) + }, new Map>()) - fetchTransferHistory = async (lockScript: Script, typeScript: Script, sizeLimit: number, lastCursor?: string) => { + fetchTransferHistory = async (lockScript: Script, typeIds: string[], sizeLimit: number, lastCursor?: string) => { const txs = await this.#indexer.getTransactions( { script: lockScript, scriptType: 'lock', - filter: { script: typeScript }, }, { order: 'desc', sizeLimit, lastCursor }, ) @@ -62,18 +85,29 @@ export class NervosService { const history = await Promise.all( txs.objects.map(async ({ txHash }) => { const { transaction } = await this.#rpc.getTransaction(txHash) - const from = await this.#filterFrom(transaction, utils.computeScriptHash(typeScript), lockMap) - const to = await this.#filterTo(transaction, utils.computeScriptHash(typeScript), lockMap) + const from = await this.#filterFrom(transaction, typeIds, lockMap) + const to = await this.#filterTo(transaction, typeIds, lockMap) return { - froms: Array.from(from.entries()).map(([lockHash, amount]) => ({ - lock: lockMap.get(lockHash)!, - amount: amount.toString(), - })), - to: Array.from(to.entries()).map(([lockHash, amount]) => ({ - lock: lockMap.get(lockHash)!, - amount: amount.toString(), - })), + txHash, + list: typeIds + .filter((typeId) => { + return from.has(typeId) || to.has(typeId) + }) + .map((typeId) => ({ + from: from.get(typeId) + ? Array.from(from.get(typeId)!.entries()).map(([lockHash, amount]) => ({ + lock: lockMap.get(lockHash)!, + amount: amount.toString(), + })) + : [], + to: to.get(typeId) + ? Array.from(to.get(typeId)!.entries()).map(([lockHash, amount]) => ({ + lock: lockMap.get(lockHash)!, + amount: amount.toString(), + })) + : [], + })), } }), ) From c7568d5ade6d5633944926a8af18e0806f693c47 Mon Sep 17 00:00:00 2001 From: daryl Date: Wed, 20 Dec 2023 11:08:57 +0800 Subject: [PATCH 31/49] feat(transaction): token history add token name --- .../samples/sudt/src/controllers/account.controller.ts | 9 +++++++++ packages/samples/sudt/src/services/nervos.service.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/packages/samples/sudt/src/controllers/account.controller.ts b/packages/samples/sudt/src/controllers/account.controller.ts index 61c605d3..b959395f 100644 --- a/packages/samples/sudt/src/controllers/account.controller.ts +++ b/packages/samples/sudt/src/controllers/account.controller.ts @@ -55,6 +55,11 @@ export class AccountController extends BaseController { return { lastCursor: '', history: [] } } + const tokenMap = tokens.reduce((acc, cur) => { + acc.set(cur.typeId, cur) + return acc + }, new Map()) + const history = await this._nervosService.fetchTransferHistory( getLock(address), tokens.map((token) => token.typeId), @@ -72,10 +77,14 @@ export class AccountController extends BaseController { ...{ list: tx.list.map((item) => ({ from: item.from.map((from) => ({ + token: tokenMap.get(item.typeId), + typeId: item.typeId, amount: from.amount, address: encodeToAddress(from.lock), })), to: item.to.map((to) => ({ + token: tokenMap.get(item.typeId), + typeId: item.typeId, amount: to.amount, address: encodeToAddress(to.lock), })), diff --git a/packages/samples/sudt/src/services/nervos.service.ts b/packages/samples/sudt/src/services/nervos.service.ts index f977c498..f601e880 100644 --- a/packages/samples/sudt/src/services/nervos.service.ts +++ b/packages/samples/sudt/src/services/nervos.service.ts @@ -95,6 +95,7 @@ export class NervosService { return from.has(typeId) || to.has(typeId) }) .map((typeId) => ({ + typeId, from: from.get(typeId) ? Array.from(from.get(typeId)!.entries()).map(([lockHash, amount]) => ({ lock: lockMap.get(lockHash)!, From 46e1e145a5d97e888f78dd08f91cb879655bef74 Mon Sep 17 00:00:00 2001 From: daryl Date: Wed, 20 Dec 2023 15:04:04 +0800 Subject: [PATCH 32/49] feat(sudt): history change to the front end --- .../src/controllers/account.controller.ts | 21 +-- .../sudt/src/services/nervos.service.ts | 136 ++++++++---------- 2 files changed, 64 insertions(+), 93 deletions(-) diff --git a/packages/samples/sudt/src/controllers/account.controller.ts b/packages/samples/sudt/src/controllers/account.controller.ts index b959395f..2b884286 100644 --- a/packages/samples/sudt/src/controllers/account.controller.ts +++ b/packages/samples/sudt/src/controllers/account.controller.ts @@ -4,7 +4,6 @@ import { Account } from '../entities/account.entity' import { SudtResponse } from '../response' import { Token } from '../entities/token.entity' import { getLock } from '../utils' -import { encodeToAddress } from '@ckb-lumos/helpers' import { Asset } from '../entities/asset.entity' import { LockModel } from '../actors/lock.model' import { NervosService } from '../services/nervos.service' @@ -75,19 +74,13 @@ export class AccountController extends BaseController { history: history.history.map((tx) => ({ ...tx, ...{ - list: tx.list.map((item) => ({ - from: item.from.map((from) => ({ - token: tokenMap.get(item.typeId), - typeId: item.typeId, - amount: from.amount, - address: encodeToAddress(from.lock), - })), - to: item.to.map((to) => ({ - token: tokenMap.get(item.typeId), - typeId: item.typeId, - amount: to.amount, - address: encodeToAddress(to.lock), - })), + from: tx.from.map((from) => ({ + ...from, + ...{ typeId: from.token, token: tokenMap.get(from.token) ?? '' }, + })), + to: tx.to.map((to) => ({ + ...to, + ...{ typeId: to.token, token: tokenMap.get(to.token) ?? '' }, })), }, })), diff --git a/packages/samples/sudt/src/services/nervos.service.ts b/packages/samples/sudt/src/services/nervos.service.ts index f601e880..79f61500 100644 --- a/packages/samples/sudt/src/services/nervos.service.ts +++ b/packages/samples/sudt/src/services/nervos.service.ts @@ -1,7 +1,12 @@ -import { BI, Indexer, Output, RPC, Script, Transaction, utils } from '@ckb-lumos/lumos' +import { BI, Indexer, RPC, Script, Transaction, utils } from '@ckb-lumos/lumos' +import { encodeToAddress } from '@ckb-lumos/helpers' -type TypeHash = string -type LockHash = string +export interface Transfer { + address: string + token: string + ckb: string + amount: string +} export class NervosService { #indexer: Indexer @@ -11,66 +16,58 @@ export class NervosService { this.#rpc = new RPC(rpcUrl) } - #collectTokenAmount = ( - map: Map, - typeId: string, - output: Output, - outputData: string, - lockMap: Map, - ): Map => { - if (output.type) { - if (utils.computeScriptHash(output.type) !== typeId) return map - - const lockHash = utils.computeScriptHash(output.lock) - lockMap.set(lockHash, output.type) - - const totalAmount = map.get(typeId) ?? BI.from(0) - map.set(lockHash, totalAmount.add(BI.from(outputData))) - } - - return map - } - - #filterFrom = async ( - tx: Transaction, - typeIds: string[], - lockMap: Map, - ): Promise>> => { - const from = new Map>() - for (const typeId of typeIds) { - let fromSingleTx = new Map() - for (const input of tx.inputs) { - const previousTransaction = await this.#rpc.getTransaction(input.previousOutput.txHash) - const txIndex = parseInt(input.previousOutput.index, 16) - const previousOutput = previousTransaction.transaction.outputs[txIndex] - const previousOutputData = previousTransaction.transaction.outputsData[txIndex] - fromSingleTx = this.#collectTokenAmount(fromSingleTx, typeId, previousOutput, previousOutputData, lockMap) - } - - if (Array.from(fromSingleTx.entries()).length > 0) { - from.set(typeId, fromSingleTx) + #filterFrom = async (tx: Transaction, typeIds: string[]): Promise => { + const from: Transfer[] = [] + for (const input of tx.inputs) { + const previousTransaction = await this.#rpc.getTransaction(input.previousOutput.txHash) + const txIndex = parseInt(input.previousOutput.index, 16) + const previousOutput = previousTransaction.transaction.outputs[txIndex] + if (previousOutput.type) { + const typeHash = utils.computeScriptHash(previousOutput.type) + if (typeIds.find((typeId) => typeHash === typeId)) { + const previousOutputData = previousTransaction.transaction.outputsData[txIndex] + from.push({ + address: encodeToAddress(previousOutput.lock), + token: typeHash, + ckb: previousOutput.capacity, + amount: BI.from(previousOutputData).toString(), + }) + } + } else { + from.push({ + address: encodeToAddress(previousOutput.lock), + token: '', + ckb: previousOutput.capacity, + amount: '0', + }) } } + return from } - #filterTo = async ( - tx: Transaction, - typeIds: string[], - lockMap: Map, - ): Promise>> => - typeIds.reduce((acc, typeId) => { - const toSingleTx = tx.outputs.reduce((acc, cur, key) => { - this.#collectTokenAmount(acc, typeId, cur, tx.outputsData[key], lockMap) - return acc - }, new Map()) - - if (Array.from(toSingleTx.entries()).length > 0) { - acc.set(typeId, toSingleTx) + #filterTo = async (tx: Transaction, typeIds: string[]): Promise => + tx.outputs.reduce((acc, cur, key) => { + if (cur.type) { + const typeHash = utils.computeScriptHash(cur.type) + if (typeIds.find((typeId) => typeHash === typeId)) { + acc.push({ + address: encodeToAddress(cur.lock), + token: typeHash, + ckb: cur.capacity, + amount: tx.outputsData[key], + }) + } + } else { + acc.push({ + address: encodeToAddress(cur.lock), + token: 'CKB', + ckb: cur.capacity, + amount: '0', + }) } - return acc - }, new Map>()) + }, []) fetchTransferHistory = async (lockScript: Script, typeIds: string[], sizeLimit: number, lastCursor?: string) => { const txs = await this.#indexer.getTransactions( @@ -80,35 +77,16 @@ export class NervosService { }, { order: 'desc', sizeLimit, lastCursor }, ) - const lockMap = new Map() - const history = await Promise.all( txs.objects.map(async ({ txHash }) => { const { transaction } = await this.#rpc.getTransaction(txHash) - const from = await this.#filterFrom(transaction, typeIds, lockMap) - const to = await this.#filterTo(transaction, typeIds, lockMap) + const from = await this.#filterFrom(transaction, typeIds) + const to = await this.#filterTo(transaction, typeIds) return { txHash, - list: typeIds - .filter((typeId) => { - return from.has(typeId) || to.has(typeId) - }) - .map((typeId) => ({ - typeId, - from: from.get(typeId) - ? Array.from(from.get(typeId)!.entries()).map(([lockHash, amount]) => ({ - lock: lockMap.get(lockHash)!, - amount: amount.toString(), - })) - : [], - to: to.get(typeId) - ? Array.from(to.get(typeId)!.entries()).map(([lockHash, amount]) => ({ - lock: lockMap.get(lockHash)!, - amount: amount.toString(), - })) - : [], - })), + from, + to, } }), ) From cc7f790b93e61ed258391bb441ea3764f7e5798d Mon Sep 17 00:00:00 2001 From: daryl Date: Sun, 24 Dec 2023 16:02:21 +0800 Subject: [PATCH 33/49] fix(sudt): need start block in config --- packages/models/src/resource-binding/manager.ts | 6 +++++- packages/models/src/resource-binding/utils.ts | 3 ++- packages/samples/sudt/kuai.config.ts | 4 +++- packages/samples/sudt/src/bootstrap.ts | 12 +++++++----- packages/samples/sudt/src/tasks/balance.task.ts | 4 ++-- packages/samples/sudt/src/type-extends.ts | 17 +++++++++-------- 6 files changed, 28 insertions(+), 18 deletions(-) diff --git a/packages/models/src/resource-binding/manager.ts b/packages/models/src/resource-binding/manager.ts index ef3096e0..bf887c22 100644 --- a/packages/models/src/resource-binding/manager.ts +++ b/packages/models/src/resource-binding/manager.ts @@ -109,8 +109,12 @@ export class Manager extends Actor, private _dataSource: ChainSource, + startBlockNumber?: string, ) { super() + if (startBlockNumber && BI.isBI(BI.from(startBlockNumber))) { + this.#tipBlockNumber = BI.from(startBlockNumber) + } } onListenBlock = (blockHeader: Header) => { @@ -233,7 +237,7 @@ export class Manager extends Actor { if (newOutputs.has(outPoint)) { diff --git a/packages/models/src/resource-binding/utils.ts b/packages/models/src/resource-binding/utils.ts index 148248ad..59a67061 100644 --- a/packages/models/src/resource-binding/utils.ts +++ b/packages/models/src/resource-binding/utils.ts @@ -15,12 +15,13 @@ export function initiateResourceBindingManager(params: { dataSource?: ChainSource listener?: Listener
rpc?: string + startBlockNumber?: string }) { assert(params.rpc || params.dataSource, 'dataSource or rpc is required') const dataSource = params.dataSource ?? new NervosChainSource(params.rpc!) const listener = params.listener ?? new TipHeaderListener(dataSource) Reflect.defineMetadata(ProviderKey.Actor, { ref: new ActorReference('resource', '/').json }, Manager) - const manager = new Manager(listener, dataSource) + const manager = new Manager(listener, dataSource, params.startBlockNumber) return { manager, ...manager.listen() } } diff --git a/packages/samples/sudt/kuai.config.ts b/packages/samples/sudt/kuai.config.ts index 61616a42..93da41cc 100644 --- a/packages/samples/sudt/kuai.config.ts +++ b/packages/samples/sudt/kuai.config.ts @@ -12,11 +12,13 @@ const REDIS_USER = redisOpt?.username ?? process.env.REDISUSER const REDIS_PASSWORD = redisOpt?.password ?? process.env.REDISPASSWORD const REDIS_HOST = process.env.REDIS_HOST ?? process.env.REDISHOST const REDIS_PORT = process.env.REDIS_PORT ?? process.env.REDISPORT +const PORT = process.env.PORT ?? process.env.PORT const redisAuth = REDIS_USER && REDIS_PASSWORD ? { username: REDIS_USER, password: REDIS_PASSWORD } : undefined const config = { - port: 3000, + port: PORT, + startBlockNumber: process.env.STARTBLOCKNUMBER, redisPort: REDIS_PORT ? +REDIS_PORT : undefined, redisHost: REDIS_HOST, network: process.env.NETWORK || 'testnet', diff --git a/packages/samples/sudt/src/bootstrap.ts b/packages/samples/sudt/src/bootstrap.ts index 2b7b9ce0..74065a4c 100644 --- a/packages/samples/sudt/src/bootstrap.ts +++ b/packages/samples/sudt/src/bootstrap.ts @@ -15,7 +15,7 @@ import { config } from '@ckb-lumos/lumos' import { DataSource } from 'typeorm' import { AccountController } from './controllers/account.controller' import { ExplorerService } from './services/explorer.service' -// import { BalanceTask } from './tasks/balance.task' +import { BalanceTask } from './tasks/balance.task' import { NervosService } from './services/nervos.service' const initiateDataSource = async () => { @@ -43,7 +43,6 @@ process.on('uncaughtException', (error) => { export const bootstrap = async () => { const kuaiCtx = await initialKuai() const kuaiEnv = kuaiCtx.getRuntimeEnvironment() - console.log(kuaiEnv.config) if (kuaiEnv.config.redisPort) { mqContainer.bind(REDIS_PORT_SYMBOL).toConstantValue(kuaiEnv.config.redisPort) @@ -68,15 +67,18 @@ export const bootstrap = async () => { const port = kuaiEnv.config?.port || 3000 - initiateResourceBindingManager({ rpc: kuaiEnv.config.ckbChain.rpcUrl }) + initiateResourceBindingManager({ + rpc: kuaiEnv.config.ckbChain.rpcUrl, + startBlockNumber: kuaiEnv.config.startBlockNumber, + }) const app = new Koa() app.use(koaBody()) const dataSource = await initiateDataSource() - // const balanceTask = new BalanceTask(dataSource) - // balanceTask.run() + const balanceTask = new BalanceTask(dataSource) + balanceTask.run() const nervosService = new NervosService(kuaiEnv.config.ckbChain.rpcUrl, kuaiEnv.config.ckbChain.rpcUrl) // init kuai io diff --git a/packages/samples/sudt/src/tasks/balance.task.ts b/packages/samples/sudt/src/tasks/balance.task.ts index d4d1a08e..80a22e89 100644 --- a/packages/samples/sudt/src/tasks/balance.task.ts +++ b/packages/samples/sudt/src/tasks/balance.task.ts @@ -37,7 +37,7 @@ export class BalanceTask { accountId: account.id, tokenId: token.id, typeId: token.typeId, - balance: balance.toString(), + balance: balance.sudtBalance.toString(), }) } else { asset.setBalance(balance.sudtBalance) @@ -49,7 +49,7 @@ export class BalanceTask { console.error(e) } } - await scheduler.wait(10000) + await scheduler.wait(1000) } } } diff --git a/packages/samples/sudt/src/type-extends.ts b/packages/samples/sudt/src/type-extends.ts index 74d17c6d..8353100f 100644 --- a/packages/samples/sudt/src/type-extends.ts +++ b/packages/samples/sudt/src/type-extends.ts @@ -1,13 +1,14 @@ -import '@ckb-js/kuai-core'; -import type { Config } from '@ckb-lumos/config-manager'; -import type { RedisOptions } from 'ioredis'; +import '@ckb-js/kuai-core' +import type { Config } from '@ckb-lumos/config-manager' +import type { RedisOptions } from 'ioredis' declare module '@ckb-js/kuai-core' { export interface KuaiConfig { - port?: number; - lumosConfig?: Config | 'aggron4' | 'lina'; - redisPort?: number; - redisHost?: string; - redisOpt?: RedisOptions; + port?: number + lumosConfig?: Config | 'aggron4' | 'lina' + redisPort?: number + redisHost?: string + redisOpt?: RedisOptions + startBlockNumber?: string } } From 91507e0a7319e7f8cb86b956c78023409ce989ab Mon Sep 17 00:00:00 2001 From: daryl Date: Mon, 25 Dec 2023 14:35:00 +0800 Subject: [PATCH 34/49] feat(sudt): assets will not be responded while user don't hold --- packages/samples/sudt/src/controllers/account.controller.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/samples/sudt/src/controllers/account.controller.ts b/packages/samples/sudt/src/controllers/account.controller.ts index 2b884286..123d94b5 100644 --- a/packages/samples/sudt/src/controllers/account.controller.ts +++ b/packages/samples/sudt/src/controllers/account.controller.ts @@ -7,6 +7,7 @@ import { getLock } from '../utils' import { Asset } from '../entities/asset.entity' import { LockModel } from '../actors/lock.model' import { NervosService } from '../services/nervos.service' +import { BI } from '@ckb-lumos/lumos' @Controller('/account') export class AccountController extends BaseController { @@ -95,7 +96,9 @@ export class AccountController extends BaseController { const assets = await this._dataSource.getRepository(Asset).findBy({ accountId: account.id }) const assetsMap = assets.reduce((acc, cur) => { - acc.set(cur.tokenId, cur) + if (BI.from(cur.balance).gt(0)) { + acc.set(cur.tokenId, cur) + } return acc }, new Map()) return tokens.map((token) => { From 453bf1a5474117016bd91282f98f2186d28381d0 Mon Sep 17 00:00:00 2001 From: daryl Date: Tue, 26 Dec 2023 15:52:07 +0800 Subject: [PATCH 35/49] feat(sudt): use acp to sudt model --- packages/samples/sudt/src/actors/sudt.model.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/samples/sudt/src/actors/sudt.model.ts b/packages/samples/sudt/src/actors/sudt.model.ts index 6cfa2e03..e06c4e33 100644 --- a/packages/samples/sudt/src/actors/sudt.model.ts +++ b/packages/samples/sudt/src/actors/sudt.model.ts @@ -16,7 +16,7 @@ import { TypeFilter, Sudt, LockFilter, - Omnilock, + DefaultScript, } from '@ckb-js/kuai-models' import type { Cell, HexString, Script } from '@ckb-lumos/base' import { number, bytes } from '@ckb-lumos/codec' @@ -31,7 +31,7 @@ import { LockModel } from './lock.model' @ActorProvider({ ref: { name: 'sudt', path: `/:typeArgs/:lockArgs/` } }) @TypeFilter() @LockFilter() -@Omnilock() +@DefaultScript('ANYONE_CAN_PAY') @Sudt() export class SudtModel extends JSONStore> { constructor( From 7bf1b30c5fc5c7dc02d8e8a6e10d42037eb586e3 Mon Sep 17 00:00:00 2001 From: daryl Date: Thu, 28 Dec 2023 11:45:55 +0800 Subject: [PATCH 36/49] feat(sudt): set owner address in token api --- .../sudt/src/controllers/sudt.controller.ts | 22 +++++++++++++++++-- packages/samples/sudt/src/dto/token.dto.ts | 4 +++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/samples/sudt/src/controllers/sudt.controller.ts b/packages/samples/sudt/src/controllers/sudt.controller.ts index 09c22b43..09858014 100644 --- a/packages/samples/sudt/src/controllers/sudt.controller.ts +++ b/packages/samples/sudt/src/controllers/sudt.controller.ts @@ -180,7 +180,8 @@ export default class SudtController extends BaseController { const token = await this._dataSource.getRepository(Token).findOneBy({ typeId }) if (token) { - return SudtResponse.ok(tokenEntityToDto(token, '0', this.#explorerHost)) + const owner = await this._dataSource.getRepository(Account).findOneBy({ id: token.ownerId }) + return SudtResponse.ok(tokenEntityToDto(token, owner?.address ?? '', '0', this.#explorerHost)) } else { throw new NotFound() } @@ -190,6 +191,23 @@ export default class SudtController extends BaseController { async listTokens() { const tokens = await this._dataSource.getRepository(Token).find() - return SudtResponse.ok(tokens.map((token) => tokenEntityToDto(token, '0', this.#explorerHost))) + const owners = await tokens.reduce(async (accP, cur) => { + const acc = await accP + if (!acc.has(cur.ownerId)) { + const owner = await this._dataSource.getRepository(Account).findOneBy({ id: cur.ownerId }) + if (owner) { + acc.set(owner.id, owner.address) + } + } + return acc + }, Promise.resolve(new Map())) + + return SudtResponse.ok( + await Promise.all( + tokens.map((token) => { + return tokenEntityToDto(token, owners.get(token.ownerId) ?? '', '0', this.#explorerHost) + }), + ), + ) } } diff --git a/packages/samples/sudt/src/dto/token.dto.ts b/packages/samples/sudt/src/dto/token.dto.ts index 5084fe70..a34c36d5 100644 --- a/packages/samples/sudt/src/dto/token.dto.ts +++ b/packages/samples/sudt/src/dto/token.dto.ts @@ -10,9 +10,10 @@ export interface TokenResponse { website: string icon: string explorerUrl: string + owner: string } -export const tokenEntityToDto = (token: Token, amount: string, explorerHost: string): TokenResponse => { +export const tokenEntityToDto = (token: Token, owner: string, amount: string, explorerHost: string): TokenResponse => { return { symbol: token.name, name: token.name, @@ -23,5 +24,6 @@ export const tokenEntityToDto = (token: Token, amount: string, explorerHost: str icon: token.icon, typeId: token.typeId, explorerUrl: `${explorerHost}/sudt/${token.typeId}`, + owner, } } From 3bcb3edb16e4320a9ede022caf8407128492ab5d Mon Sep 17 00:00:00 2001 From: daryl Date: Thu, 28 Dec 2023 12:03:55 +0800 Subject: [PATCH 37/49] fix(sudt): filter the token which users don't have --- .../src/controllers/account.controller.ts | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/samples/sudt/src/controllers/account.controller.ts b/packages/samples/sudt/src/controllers/account.controller.ts index 123d94b5..28a71cda 100644 --- a/packages/samples/sudt/src/controllers/account.controller.ts +++ b/packages/samples/sudt/src/controllers/account.controller.ts @@ -101,18 +101,21 @@ export class AccountController extends BaseController { } return acc }, new Map()) - return tokens.map((token) => { - try { - return { - uan: token.name, - displayName: token.name, - decimal: token.decimal, - amount: assetsMap.get(token.id)?.balance ?? '0', - typeId: token.typeId, + + return tokens + .filter((token) => BI.from(assetsMap.get(token.id)?.balance ?? 0).gt(0)) + .map((token) => { + try { + return { + uan: token.name, + displayName: token.name, + decimal: token.decimal, + amount: assetsMap.get(token.id)?.balance ?? '0', + typeId: token.typeId, + } + } catch (e) { + console.error(e) } - } catch (e) { - console.error(e) - } - }) + }) } } From d2529c80eaec1810bf34e46202d230784dd9d9c7 Mon Sep 17 00:00:00 2001 From: painterpuppets Date: Sun, 31 Dec 2023 12:21:08 +0800 Subject: [PATCH 38/49] fix: history api amount bug & history tx duplicated bug --- .../src/controllers/account.controller.ts | 21 ++++------ .../sudt/src/services/nervos.service.ts | 39 ++++++++++++------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/packages/samples/sudt/src/controllers/account.controller.ts b/packages/samples/sudt/src/controllers/account.controller.ts index 28a71cda..b9bbb97c 100644 --- a/packages/samples/sudt/src/controllers/account.controller.ts +++ b/packages/samples/sudt/src/controllers/account.controller.ts @@ -47,27 +47,22 @@ export class AccountController extends BaseController { async accountTransaction( @Param('address') address: string, @Query('size') size: number, + @Query('typeId') typeId: string, @Query('lastCursor') lastCursor?: string, ) { const tokens = await this._dataSource.getRepository(Token).find() - if (tokens.length === 0) { - return { lastCursor: '', history: [] } - } - const tokenMap = tokens.reduce((acc, cur) => { acc.set(cur.typeId, cur) return acc }, new Map()) - const history = await this._nervosService.fetchTransferHistory( - getLock(address), - tokens.map((token) => token.typeId), - size, + const history = await this._nervosService.fetchTransferHistory({ + lockScript: getLock(address), + typeIds: tokens.map((token) => token.typeId), + sizeLimit: size, lastCursor, - ) - - console.log(history) + }) return { ...history, @@ -77,11 +72,11 @@ export class AccountController extends BaseController { ...{ from: tx.from.map((from) => ({ ...from, - ...{ typeId: from.token, token: tokenMap.get(from.token) ?? '' }, + ...{ typeId: from.typeId, token: from.typeId ? tokenMap.get(from.typeId) ?? undefined : undefined }, })), to: tx.to.map((to) => ({ ...to, - ...{ typeId: to.token, token: tokenMap.get(to.token) ?? '' }, + ...{ typeId: to.typeId, token: to.typeId ? tokenMap.get(to.typeId) ?? undefined : undefined }, })), }, })), diff --git a/packages/samples/sudt/src/services/nervos.service.ts b/packages/samples/sudt/src/services/nervos.service.ts index 79f61500..918c0b9a 100644 --- a/packages/samples/sudt/src/services/nervos.service.ts +++ b/packages/samples/sudt/src/services/nervos.service.ts @@ -1,11 +1,12 @@ import { BI, Indexer, RPC, Script, Transaction, utils } from '@ckb-lumos/lumos' import { encodeToAddress } from '@ckb-lumos/helpers' +import { number } from '@ckb-lumos/codec' export interface Transfer { address: string - token: string + typeId?: string ckb: string - amount: string + amount?: string } export class NervosService { @@ -23,22 +24,20 @@ export class NervosService { const txIndex = parseInt(input.previousOutput.index, 16) const previousOutput = previousTransaction.transaction.outputs[txIndex] if (previousOutput.type) { - const typeHash = utils.computeScriptHash(previousOutput.type) - if (typeIds.find((typeId) => typeHash === typeId)) { + const typeId = utils.computeScriptHash(previousOutput.type) + if (typeIds.find((i) => typeId === i)) { const previousOutputData = previousTransaction.transaction.outputsData[txIndex] from.push({ address: encodeToAddress(previousOutput.lock), - token: typeHash, + typeId, ckb: previousOutput.capacity, - amount: BI.from(previousOutputData).toString(), + amount: BI.from(number.Uint128LE.unpack(previousOutputData.slice(0, 34))).toString(), }) } } else { from.push({ address: encodeToAddress(previousOutput.lock), - token: '', ckb: previousOutput.capacity, - amount: '0', }) } } @@ -49,34 +48,44 @@ export class NervosService { #filterTo = async (tx: Transaction, typeIds: string[]): Promise => tx.outputs.reduce((acc, cur, key) => { if (cur.type) { - const typeHash = utils.computeScriptHash(cur.type) - if (typeIds.find((typeId) => typeHash === typeId)) { + const typeId = utils.computeScriptHash(cur.type) + if (typeIds.find((i) => typeId === i)) { acc.push({ address: encodeToAddress(cur.lock), - token: typeHash, + typeId, ckb: cur.capacity, - amount: tx.outputsData[key], + amount: BI.from(number.Uint128LE.unpack(tx.outputsData[key].slice(0, 34))).toString(), }) } } else { acc.push({ address: encodeToAddress(cur.lock), - token: 'CKB', ckb: cur.capacity, - amount: '0', }) } return acc }, []) - fetchTransferHistory = async (lockScript: Script, typeIds: string[], sizeLimit: number, lastCursor?: string) => { + fetchTransferHistory = async ({ + lockScript, + typeIds, + sizeLimit, + lastCursor, + }: { + lockScript: Script + typeIds: string[] + sizeLimit: number + lastCursor?: string + }) => { const txs = await this.#indexer.getTransactions( { script: lockScript, scriptType: 'lock', + groupByTransaction: true, }, { order: 'desc', sizeLimit, lastCursor }, ) + const history = await Promise.all( txs.objects.map(async ({ txHash }) => { const { transaction } = await this.#rpc.getTransaction(txHash) From bdf6aa41a2994f903f42e9a65474dcab01f9ed7d Mon Sep 17 00:00:00 2001 From: daryl Date: Mon, 8 Jan 2024 00:55:33 +0800 Subject: [PATCH 39/49] fix(sudt): submit the token info to explorer in task --- .../samples/sudt/src/actors/token.model.ts | 34 +++++++++++++ packages/samples/sudt/src/bootstrap.ts | 3 ++ .../sudt/src/services/explorer.service.ts | 1 - packages/samples/sudt/src/tasks/token.task.ts | 51 +++++++++++++++++++ 4 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 packages/samples/sudt/src/actors/token.model.ts create mode 100644 packages/samples/sudt/src/tasks/token.task.ts diff --git a/packages/samples/sudt/src/actors/token.model.ts b/packages/samples/sudt/src/actors/token.model.ts new file mode 100644 index 00000000..92117967 --- /dev/null +++ b/packages/samples/sudt/src/actors/token.model.ts @@ -0,0 +1,34 @@ +import { + ActorProvider, + ActorReference, + CellPattern, + JSONStore, + OutPointString, + SchemaPattern, + Sudt, + Param, + TypeFilter, + UpdateStorageValue, +} from '@ckb-js/kuai-models' + +@ActorProvider({ ref: { name: 'token', path: `/:typeArgs/` } }) +@TypeFilter() +@Sudt() +export class TokenModel extends JSONStore> { + constructor( + @Param('typeArgs') typeArgs: string, + _schemaOption?: void, + params?: { + states?: Record + chainData?: Record + cellPattern?: CellPattern + schemaPattern?: SchemaPattern + }, + ) { + super(undefined, { ...params, ref: ActorReference.newWithFilter(TokenModel, `/${typeArgs}/`) }) + if (!this.typeScript) { + throw new Error('type script is required') + } + this.registerResourceBinding() + } +} diff --git a/packages/samples/sudt/src/bootstrap.ts b/packages/samples/sudt/src/bootstrap.ts index 74065a4c..eb0b17a4 100644 --- a/packages/samples/sudt/src/bootstrap.ts +++ b/packages/samples/sudt/src/bootstrap.ts @@ -17,6 +17,7 @@ import { AccountController } from './controllers/account.controller' import { ExplorerService } from './services/explorer.service' import { BalanceTask } from './tasks/balance.task' import { NervosService } from './services/nervos.service' +import { TokenTask } from './tasks/token.task' const initiateDataSource = async () => { const dataSource = new DataSource({ @@ -79,6 +80,8 @@ export const bootstrap = async () => { const balanceTask = new BalanceTask(dataSource) balanceTask.run() + const tokenTask = new TokenTask(dataSource, new ExplorerService(process.env.EXPLORER_API_HOST)) + tokenTask.run() const nervosService = new NervosService(kuaiEnv.config.ckbChain.rpcUrl, kuaiEnv.config.ckbChain.rpcUrl) // init kuai io diff --git a/packages/samples/sudt/src/services/explorer.service.ts b/packages/samples/sudt/src/services/explorer.service.ts index 3018ff17..be2d4742 100644 --- a/packages/samples/sudt/src/services/explorer.service.ts +++ b/packages/samples/sudt/src/services/explorer.service.ts @@ -12,7 +12,6 @@ export class ExplorerService { iconFile: string uan: string displayName: string - email: string token?: string }) => { const res = await fetch(`${this.host}/api/v1/udts/${params.typeHash}`, { diff --git a/packages/samples/sudt/src/tasks/token.task.ts b/packages/samples/sudt/src/tasks/token.task.ts new file mode 100644 index 00000000..8adafb10 --- /dev/null +++ b/packages/samples/sudt/src/tasks/token.task.ts @@ -0,0 +1,51 @@ +import { DataSource, Repository } from 'typeorm' +import { Token, TokenStatus } from '../entities/token.entity' +import { appRegistry } from '../actors' +import { TokenModel } from '../actors/token.model' +import { ActorReference } from '@ckb-js/kuai-models' +import { ExplorerService } from '../services/explorer.service' + +export class TokenTask { + #tokenRepo: Repository + + constructor( + dataSource: DataSource, + private _explorerService: ExplorerService, + private _maxTimeFromCreate = 60 * 10 * 1000, + ) { + this.#tokenRepo = dataSource.getRepository(Token) + } + + run = async () => { + for (;;) { + const newTokens = await this.#tokenRepo.findBy({ status: TokenStatus.New }) + for (const token of newTokens) { + const tokenModel = appRegistry.findOrBind(new ActorReference('token', `/${token.args}/`)) + if (tokenModel) { + if (Date.now() - token.createdAt.getTime() > this._maxTimeFromCreate) { + continue + } + try { + await this._explorerService.updateSUDT({ + typeHash: token.typeId, + symbol: token.name, + fullName: token.name, + decimal: token.decimal.toString(), + totalAmount: '0', + description: token.description ?? '', + operatorWebsite: token.website, + iconFile: token.icon, + uan: `${token.name}.ckb`, + displayName: token.name, + }) + + token.status = TokenStatus.Committed + await this.#tokenRepo.save(token) + } catch (e) { + console.error(e) + } + } + } + } + } +} From 4e2bdbc76fdad827035a37c9f5bed912403a437c Mon Sep 17 00:00:00 2001 From: daryl Date: Mon, 8 Jan 2024 01:23:55 +0800 Subject: [PATCH 40/49] feat(sudt): filter the tx which token not included Signed-off-by: daryl --- .../samples/sudt/src/controllers/sudt.controller.ts | 1 - packages/samples/sudt/src/services/nervos.service.ts | 12 +----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/samples/sudt/src/controllers/sudt.controller.ts b/packages/samples/sudt/src/controllers/sudt.controller.ts index 09858014..8a915e1a 100644 --- a/packages/samples/sudt/src/controllers/sudt.controller.ts +++ b/packages/samples/sudt/src/controllers/sudt.controller.ts @@ -163,7 +163,6 @@ export default class SudtController extends BaseController { iconFile: req.icon, uan: `${req.name}.ckb`, displayName: req.name, - email: req.email, token: req.explorerCode, }) diff --git a/packages/samples/sudt/src/services/nervos.service.ts b/packages/samples/sudt/src/services/nervos.service.ts index 918c0b9a..aa250abe 100644 --- a/packages/samples/sudt/src/services/nervos.service.ts +++ b/packages/samples/sudt/src/services/nervos.service.ts @@ -34,11 +34,6 @@ export class NervosService { amount: BI.from(number.Uint128LE.unpack(previousOutputData.slice(0, 34))).toString(), }) } - } else { - from.push({ - address: encodeToAddress(previousOutput.lock), - ckb: previousOutput.capacity, - }) } } @@ -57,11 +52,6 @@ export class NervosService { amount: BI.from(number.Uint128LE.unpack(tx.outputsData[key].slice(0, 34))).toString(), }) } - } else { - acc.push({ - address: encodeToAddress(cur.lock), - ckb: cur.capacity, - }) } return acc }, []) @@ -102,7 +92,7 @@ export class NervosService { return { lastCursor: txs.lastCursor, - history, + history: history.filter((tx) => tx.from.length > 0 || tx.to.length > 0), } } } From 5b3f6af6d4004aa68b4b7c8920cf311d185b8c3e Mon Sep 17 00:00:00 2001 From: daryl Date: Mon, 8 Jan 2024 10:34:44 +0800 Subject: [PATCH 41/49] fix(sudt): explorer service put method --- packages/samples/sudt/src/services/explorer.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/samples/sudt/src/services/explorer.service.ts b/packages/samples/sudt/src/services/explorer.service.ts index be2d4742..0a49811e 100644 --- a/packages/samples/sudt/src/services/explorer.service.ts +++ b/packages/samples/sudt/src/services/explorer.service.ts @@ -15,7 +15,7 @@ export class ExplorerService { token?: string }) => { const res = await fetch(`${this.host}/api/v1/udts/${params.typeHash}`, { - method: 'POST', + method: 'PUT', body: JSON.stringify({}), headers: { Accept: 'application/vnd.api+json', From 8af44e2640e8514d9b07b25145c6f7975a9e8e41 Mon Sep 17 00:00:00 2001 From: daryl Date: Mon, 8 Jan 2024 17:05:31 +0800 Subject: [PATCH 42/49] fix(sudt): explorer update token info api --- packages/samples/sudt/src/bootstrap.ts | 7 +++++-- .../sudt/src/controllers/sudt.controller.ts | 5 ----- .../sudt/src/services/explorer.service.ts | 20 +++++++++++++------ packages/samples/sudt/src/tasks/token.task.ts | 5 +---- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/samples/sudt/src/bootstrap.ts b/packages/samples/sudt/src/bootstrap.ts index eb0b17a4..fab93981 100644 --- a/packages/samples/sudt/src/bootstrap.ts +++ b/packages/samples/sudt/src/bootstrap.ts @@ -80,13 +80,16 @@ export const bootstrap = async () => { const balanceTask = new BalanceTask(dataSource) balanceTask.run() - const tokenTask = new TokenTask(dataSource, new ExplorerService(process.env.EXPLORER_API_HOST)) + const tokenTask = new TokenTask(dataSource, new ExplorerService(process.env.EXPLORER_API_HOST, process.env.EMAIL!)) tokenTask.run() const nervosService = new NervosService(kuaiEnv.config.ckbChain.rpcUrl, kuaiEnv.config.ckbChain.rpcUrl) // init kuai io const cor = new CoR() - const sudtController = new SudtController(dataSource, new ExplorerService(process.env.EXPLORER_API_HOST)) + const sudtController = new SudtController( + dataSource, + new ExplorerService(process.env.EXPLORER_API_HOST, process.env.EMAIL!), + ) const accountController = new AccountController(dataSource, nervosService) cor.use(sudtController.middleware()) cor.use(accountController.middleware()) diff --git a/packages/samples/sudt/src/controllers/sudt.controller.ts b/packages/samples/sudt/src/controllers/sudt.controller.ts index 8a915e1a..d9fe68fa 100644 --- a/packages/samples/sudt/src/controllers/sudt.controller.ts +++ b/packages/samples/sudt/src/controllers/sudt.controller.ts @@ -155,15 +155,10 @@ export default class SudtController extends BaseController { this._explorerService.updateSUDT({ typeHash: typeId, symbol: req.name, - fullName: req.name, decimal: req.decimal.toString(), totalAmount: '0', description: req.description, operatorWebsite: req.website, - iconFile: req.icon, - uan: `${req.name}.ckb`, - displayName: req.name, - token: req.explorerCode, }) try { diff --git a/packages/samples/sudt/src/services/explorer.service.ts b/packages/samples/sudt/src/services/explorer.service.ts index 0a49811e..b3659b28 100644 --- a/packages/samples/sudt/src/services/explorer.service.ts +++ b/packages/samples/sudt/src/services/explorer.service.ts @@ -1,22 +1,30 @@ export class ExplorerService { - constructor(private host = 'https://testnet-api.explorer.nervos.org') {} + constructor( + private host = 'https://testnet-api.explorer.nervos.org', + private _email: string, + ) {} updateSUDT = async (params: { typeHash: string symbol: string - fullName: string decimal: string totalAmount: string description: string operatorWebsite: string - iconFile: string - uan: string - displayName: string token?: string }) => { const res = await fetch(`${this.host}/api/v1/udts/${params.typeHash}`, { method: 'PUT', - body: JSON.stringify({}), + body: JSON.stringify({ + type_hash: params.typeHash, + symbol: params.symbol, + decimal: params.decimal, + total_amount: params.totalAmount, + description: params.description, + operator_website: params.operatorWebsite, + email: this._email, + uan: `${params.symbol}.ckb`, + }), headers: { Accept: 'application/vnd.api+json', 'Content-Type': 'application/vnd.api+json', diff --git a/packages/samples/sudt/src/tasks/token.task.ts b/packages/samples/sudt/src/tasks/token.task.ts index 8adafb10..9d823084 100644 --- a/packages/samples/sudt/src/tasks/token.task.ts +++ b/packages/samples/sudt/src/tasks/token.task.ts @@ -26,17 +26,14 @@ export class TokenTask { continue } try { + console.log(token) await this._explorerService.updateSUDT({ typeHash: token.typeId, symbol: token.name, - fullName: token.name, decimal: token.decimal.toString(), totalAmount: '0', description: token.description ?? '', operatorWebsite: token.website, - iconFile: token.icon, - uan: `${token.name}.ckb`, - displayName: token.name, }) token.status = TokenStatus.Committed From def258b947d51d7e87b39392c14cf193f2a96925 Mon Sep 17 00:00:00 2001 From: daryl Date: Mon, 8 Jan 2024 17:14:50 +0800 Subject: [PATCH 43/49] test --- .../sudt/src/services/explorer.service.ts | 22 ++++++++++--------- packages/samples/sudt/src/tasks/token.task.ts | 1 - 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/samples/sudt/src/services/explorer.service.ts b/packages/samples/sudt/src/services/explorer.service.ts index b3659b28..0cfa3e11 100644 --- a/packages/samples/sudt/src/services/explorer.service.ts +++ b/packages/samples/sudt/src/services/explorer.service.ts @@ -13,23 +13,25 @@ export class ExplorerService { operatorWebsite: string token?: string }) => { + const body = JSON.stringify({ + type_hash: params.typeHash, + symbol: params.symbol, + decimal: params.decimal, + total_amount: params.totalAmount, + description: params.description, + operator_website: params.operatorWebsite, + email: this._email, + uan: `${params.symbol}.ckb`, + }) const res = await fetch(`${this.host}/api/v1/udts/${params.typeHash}`, { method: 'PUT', - body: JSON.stringify({ - type_hash: params.typeHash, - symbol: params.symbol, - decimal: params.decimal, - total_amount: params.totalAmount, - description: params.description, - operator_website: params.operatorWebsite, - email: this._email, - uan: `${params.symbol}.ckb`, - }), + body, headers: { Accept: 'application/vnd.api+json', 'Content-Type': 'application/vnd.api+json', }, }) + console.log(res, body) if (!res.ok) { throw new Error('Internal Service Error') } diff --git a/packages/samples/sudt/src/tasks/token.task.ts b/packages/samples/sudt/src/tasks/token.task.ts index 9d823084..717aa28b 100644 --- a/packages/samples/sudt/src/tasks/token.task.ts +++ b/packages/samples/sudt/src/tasks/token.task.ts @@ -26,7 +26,6 @@ export class TokenTask { continue } try { - console.log(token) await this._explorerService.updateSUDT({ typeHash: token.typeId, symbol: token.name, From ee22e3bde69b360b7c962ba55b5db45c47fa5a64 Mon Sep 17 00:00:00 2001 From: daryl Date: Mon, 8 Jan 2024 18:32:11 +0800 Subject: [PATCH 44/49] test --- packages/samples/sudt/src/services/explorer.service.ts | 1 - packages/samples/sudt/src/tasks/token.task.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/samples/sudt/src/services/explorer.service.ts b/packages/samples/sudt/src/services/explorer.service.ts index 0cfa3e11..ab743dc9 100644 --- a/packages/samples/sudt/src/services/explorer.service.ts +++ b/packages/samples/sudt/src/services/explorer.service.ts @@ -31,7 +31,6 @@ export class ExplorerService { 'Content-Type': 'application/vnd.api+json', }, }) - console.log(res, body) if (!res.ok) { throw new Error('Internal Service Error') } diff --git a/packages/samples/sudt/src/tasks/token.task.ts b/packages/samples/sudt/src/tasks/token.task.ts index 717aa28b..ac22a8e0 100644 --- a/packages/samples/sudt/src/tasks/token.task.ts +++ b/packages/samples/sudt/src/tasks/token.task.ts @@ -25,6 +25,7 @@ export class TokenTask { if (Date.now() - token.createdAt.getTime() > this._maxTimeFromCreate) { continue } + console.log(Date.now(), token.createdAt.getTime()) try { await this._explorerService.updateSUDT({ typeHash: token.typeId, From 9d3c7fbe5be2d2a4f7bebad90d8dc55d46212f68 Mon Sep 17 00:00:00 2001 From: daryl Date: Mon, 8 Jan 2024 18:40:50 +0800 Subject: [PATCH 45/49] fix(sudt): typeorm timezone --- packages/samples/sudt/src/bootstrap.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/samples/sudt/src/bootstrap.ts b/packages/samples/sudt/src/bootstrap.ts index fab93981..ab09842d 100644 --- a/packages/samples/sudt/src/bootstrap.ts +++ b/packages/samples/sudt/src/bootstrap.ts @@ -29,6 +29,7 @@ const initiateDataSource = async () => { password: process.env.DBPASSWORD || 'root', database: process.env.DBDATABASE || 'sudt', entities: [__dirname + '/entities/*.{js,ts}'], + timezone: 'Z', synchronize: true, }) From d81b797a101467770a3ea68685b46f309a3d3afe Mon Sep 17 00:00:00 2001 From: daryl Date: Mon, 8 Jan 2024 19:49:53 +0800 Subject: [PATCH 46/49] fix(sudt): explorer error --- packages/samples/sudt/src/services/explorer.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/samples/sudt/src/services/explorer.service.ts b/packages/samples/sudt/src/services/explorer.service.ts index ab743dc9..ed359f75 100644 --- a/packages/samples/sudt/src/services/explorer.service.ts +++ b/packages/samples/sudt/src/services/explorer.service.ts @@ -37,11 +37,11 @@ export class ExplorerService { switch (Math.ceil(res.status / 100)) { case 2: - return true case 3: - throw new Error('Redirect') + return true case 4: - throw new Error('Client Error') + case 5: + throw new Error(`${res.status}, ${res.statusText}`) } } } From 46b34aa22e404a3da12da58d5f3afa95c6caba76 Mon Sep 17 00:00:00 2001 From: daryl Date: Mon, 8 Jan 2024 20:02:05 +0800 Subject: [PATCH 47/49] test --- packages/samples/sudt/src/services/explorer.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/samples/sudt/src/services/explorer.service.ts b/packages/samples/sudt/src/services/explorer.service.ts index ed359f75..40832062 100644 --- a/packages/samples/sudt/src/services/explorer.service.ts +++ b/packages/samples/sudt/src/services/explorer.service.ts @@ -23,6 +23,7 @@ export class ExplorerService { email: this._email, uan: `${params.symbol}.ckb`, }) + console.log(`${this.host}/api/v1/udts/${params.typeHash}`) const res = await fetch(`${this.host}/api/v1/udts/${params.typeHash}`, { method: 'PUT', body, From 1f71795e35ec6b1dbfdc5e242787dc55f52ac012 Mon Sep 17 00:00:00 2001 From: daryl Date: Mon, 8 Jan 2024 20:21:53 +0800 Subject: [PATCH 48/49] fix(sudt): explorer error --- packages/samples/sudt/src/services/explorer.service.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/samples/sudt/src/services/explorer.service.ts b/packages/samples/sudt/src/services/explorer.service.ts index 40832062..06dd3d26 100644 --- a/packages/samples/sudt/src/services/explorer.service.ts +++ b/packages/samples/sudt/src/services/explorer.service.ts @@ -32,9 +32,6 @@ export class ExplorerService { 'Content-Type': 'application/vnd.api+json', }, }) - if (!res.ok) { - throw new Error('Internal Service Error') - } switch (Math.ceil(res.status / 100)) { case 2: From a35f270cd00f6ea619a483dbc775d4d573a2b83c Mon Sep 17 00:00:00 2001 From: daryl Date: Wed, 10 Jan 2024 11:59:45 +0800 Subject: [PATCH 49/49] fix(sudt): error amount --- packages/samples/sudt/src/controllers/sudt.controller.ts | 4 ++-- packages/samples/sudt/src/tasks/token.task.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/samples/sudt/src/controllers/sudt.controller.ts b/packages/samples/sudt/src/controllers/sudt.controller.ts index d9fe68fa..7b694ae4 100644 --- a/packages/samples/sudt/src/controllers/sudt.controller.ts +++ b/packages/samples/sudt/src/controllers/sudt.controller.ts @@ -106,8 +106,8 @@ export default class SudtController extends BaseController { .save(this._dataSource.getRepository(Account).create({ address: req.account })) } - const amount = BI.isBI(req.amount) ? BI.from(req.amount) : BI.from(0) try { + const amount = BI.from(req.amount) const lockModel = LockModel.getLock(req.account) const { typeScript, ...result } = lockModel.mint(getLock(req.account), amount) @@ -141,7 +141,7 @@ export default class SudtController extends BaseController { } console.error(e) - throw e + return SudtResponse.err('500', { message: (e as Error).message }) } } diff --git a/packages/samples/sudt/src/tasks/token.task.ts b/packages/samples/sudt/src/tasks/token.task.ts index ac22a8e0..717aa28b 100644 --- a/packages/samples/sudt/src/tasks/token.task.ts +++ b/packages/samples/sudt/src/tasks/token.task.ts @@ -25,7 +25,6 @@ export class TokenTask { if (Date.now() - token.createdAt.getTime() > this._maxTimeFromCreate) { continue } - console.log(Date.now(), token.createdAt.getTime()) try { await this._explorerService.updateSUDT({ typeHash: token.typeId,