diff --git a/package-lock.json b/package-lock.json index a3322dd6..f1a6a6c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6540,6 +6540,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", @@ -7589,6 +7597,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", @@ -9645,6 +9658,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", @@ -10296,6 +10314,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", @@ -10307,6 +10370,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", @@ -13009,6 +13091,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", @@ -15263,7 +15350,10 @@ "@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", + "mysql": "npm:mysql2@3.6.1", + "mysql2": "3.6.1", + "typeorm": "0.3.17" }, "devDependencies": { "ts-node": "10.9.2", @@ -15294,6 +15384,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", @@ -15327,6 +15428,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/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/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/.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 diff --git a/packages/samples/sudt/README.md b/packages/samples/sudt/README.md index 0242eaa0..316f62f4 100644 --- a/packages/samples/sudt/README.md +++ b/packages/samples/sudt/README.md @@ -9,3 +9,478 @@ npm run build 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 + +method: POST + +#### Request + +```javascript +{ + "code": 200, + "data": { + "name": "USDT", + "account": "", // the address of owner + "decimal": 18, + "description": "", + "website": "", + "icon": "", + "email": "" + } +} +``` + +#### Reponse + +```javascript +{ + "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/:typeId + +method: PUT + +#### Request + +```javascript +{ + "code": 200, + "data": { + "name": "USDT", + "decimal": 18, + "description": "", + "website": "", + "icon": "", + "explorerCode": "" // the verify code from explorer + } +} +``` + +#### Response + +```javascript +{ + "code": 201, + "data": {} +} +``` + +### Transfer Token + +path: /token/transfer + +method: POST + +#### Request + +```javascript +{ + "typeId": "", // token args + "amount": "", + "to": "" +} +``` + +#### Response + +```javascript +{ + "code": 200, + "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": {} + } + } +} +``` + +### Token List + +path: /token + +#### Request + +| param | type | position | description | +| ------- | ------ | -------- | ------------ | +| address | string | query | user address | + +method: GET + +#### Response + +```javascript +{ + "code": 200, + "data": [ + { + "uan": "USDT", + "displayName": "USDT", + "name": "USDT", + "decimal": 18, + "description": "", + "website": "", + "icon": "", + "url": "", + "issuser": "", + "args": "", + "typeId": "", + }, + ] +} +``` + +### Token Detail + +path: /token/:args + +method: GET + +#### Response + +```javascript +{ + "code": 200, + "data": { + "uan": "USDT", + "displayName": "USDT", + "name": "USDT", + "decimal": 18, + "description": "", + "website": "", + "icon": "", + "url": "", + "issuser": "" + } +} +``` + +### Asset List + +path: /account/:address/assets + +method: GET + +#### Request + +| param | type | position | description | +| ------- | ------ | -------- | ------------ | +| address | string | query | user address | + +#### Response + +```javascript +{ + "code": 200, + "data": [ + { + "uan": "USDT", + "displayName": "USDT", + "decimal": 18, + "amount": "" + } + ] +} +``` + +### Token Transfer History + +path: /account/:address/assets/transfer/history + +method: GET + +#### Response + +```javascript +{ + "code": 200, + "data": [ + { + "txHash": "", + "from": "", + "to": "", + "time": "", + "status": "", + "sudtAmount": "", + "CKBAmount": "", + "url": "", + } + ] +} +``` diff --git a/packages/samples/sudt/kuai.config.ts b/packages/samples/sudt/kuai.config.ts index 665f0ab1..93da41cc 100644 --- a/packages/samples/sudt/kuai.config.ts +++ b/packages/samples/sudt/kuai.config.ts @@ -1,26 +1,27 @@ -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 PORT = process.env.PORT ?? process.env.PORT -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 = { - port: 3000, - redisPort: REDIS_HOST ? +REDIS_HOST : undefined, - redisHost: REDIS_PORT, +const config = { + port: PORT, + startBlockNumber: process.env.STARTBLOCKNUMBER, + redisPort: REDIS_PORT ? +REDIS_PORT : undefined, + redisHost: REDIS_HOST, + network: process.env.NETWORK || 'testnet', redisOpt: redisOpt || redisAuth ? { @@ -28,11 +29,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/package.json b/packages/samples/sudt/package.json index 541aa1e1..14ca15f3 100644 --- a/packages/samples/sudt/package.json +++ b/packages/samples/sudt/package.json @@ -1,12 +1,15 @@ { "name": "sudt", + "private": true, + "version": "0.0.1-alpha.2", "scripts": { "dev": "ts-node src/main.ts", "build": "tsc", "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 +18,10 @@ "@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", + "mysql": "npm:mysql2@3.6.1", + "mysql2": "3.6.1", + "typeorm": "0.3.17" }, "devDependencies": { "ts-node": "10.9.2", 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) + } +} 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..c3cadf7c --- /dev/null +++ b/packages/samples/sudt/src/actors/lock.model.ts @@ -0,0 +1,27 @@ +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 { 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}/`)) + } 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 1fc5dff7..00d07989 100644 --- a/packages/samples/sudt/src/actors/omnilock.model.ts +++ b/packages/samples/sudt/src/actors/omnilock.model.ts @@ -4,14 +4,13 @@ * 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, 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, @@ -58,40 +58,50 @@ export class OmnilockModel extends JSONStore> { } } - mint( + 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: 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: utils.computeScriptHash(this.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) // 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, inputs: inputs.map((v) => v.cell), outputs: [ sudtCell, diff --git a/packages/samples/sudt/src/actors/sudt.model.ts b/packages/samples/sudt/src/actors/sudt.model.ts index 1b9dc81f..e06c4e33 100644 --- a/packages/samples/sudt/src/actors/sudt.model.ts +++ b/packages/samples/sudt/src/actors/sudt.model.ts @@ -15,22 +15,28 @@ import { UpdateStorageValue, TypeFilter, Sudt, + LockFilter, + DefaultScript, } from '@ckb-js/kuai-models' import type { Cell, HexString, Script } from '@ckb-lumos/base' 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 { LockModel } from './lock.model' /** * add business logic in an actor */ -@ActorProvider({ ref: { name: 'sudt', path: `/:args/` } }) +@ActorProvider({ ref: { name: 'sudt', path: `/:typeArgs/:lockArgs/` } }) @TypeFilter() +@LockFilter() +@DefaultScript('ANYONE_CAN_PAY') @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,30 +45,27 @@ export class SudtModel extends JSONStore> { schemaPattern?: SchemaPattern }, ) { - super(undefined, { ...params, ref: ActorReference.newWithFilter(SudtModel, `/${args}/`) }) + 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: LockModel, lockScript: Script, amount: HexString) { const CONFIG = config.getConfig() const sudtCell: Cell = { cellOutput: { @@ -81,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)).inputs) + } if (currentTotalSudt.lt(amount)) throw new InternalServerError('not enough sudt balance') const leftSudt = currentTotalSudt.sub(amount) 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 new file mode 100644 index 00000000..ab09842d --- /dev/null +++ b/packages/samples/sudt/src/bootstrap.ts @@ -0,0 +1,125 @@ +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' +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' +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({ + connectorPackage: 'mysql2', + type: 'mysql', + 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}'], + timezone: 'Z', + synchronize: true, + }) + + await dataSource.initialize() + + return dataSource +} + +process.on('uncaughtException', (error) => { + console.log(error) +}) + +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, + startBlockNumber: kuaiEnv.config.startBlockNumber, + }) + + const app = new Koa() + app.use(koaBody()) + + const dataSource = await initiateDataSource() + + const balanceTask = new BalanceTask(dataSource) + balanceTask.run() + 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, process.env.EMAIL!), + ) + const accountController = new AccountController(dataSource, nervosService) + cor.use(sudtController.middleware()) + cor.use(accountController.middleware()) + + const koaRouterAdapter = new KoaRouterAdapter(cor) + + app.use(cors()) + app.use(koaRouterAdapter.routes()) + + // 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/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..b9bbb97c --- /dev/null +++ b/packages/samples/sudt/src/controllers/account.controller.ts @@ -0,0 +1,116 @@ +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 { 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 { + #explorerHost = process.env.EXPLORER_HOST || 'https://explorer.nervos.org' + constructor( + private _dataSource: DataSource, + private _nervosService: NervosService, + ) { + super() + } + + async getOrCreateAccount(address: string) { + const repo = this._dataSource.getRepository(Account) + const account = await repo.findOneBy({ address }) + if (account) { + return account + } + + LockModel.getLock(address) + + return repo.save(repo.create({ address })) + } + + @Get('/meta/:address') + async meta(@Param('address') address: string) { + if (!address) { + throw new Request('invalid address') + } + await this.getOrCreateAccount(address) + + const lockModel = LockModel.getLock(address) + + return SudtResponse.ok(lockModel?.meta) + } + + @Get('/:address/assets/transaction') + 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() + + const tokenMap = tokens.reduce((acc, cur) => { + acc.set(cur.typeId, cur) + return acc + }, new Map()) + + const history = await this._nervosService.fetchTransferHistory({ + lockScript: getLock(address), + typeIds: tokens.map((token) => token.typeId), + sizeLimit: size, + lastCursor, + }) + + return { + ...history, + ...{ + history: history.history.map((tx) => ({ + ...tx, + ...{ + from: tx.from.map((from) => ({ + ...from, + ...{ typeId: from.typeId, token: from.typeId ? tokenMap.get(from.typeId) ?? undefined : undefined }, + })), + to: tx.to.map((to) => ({ + ...to, + ...{ typeId: to.typeId, token: to.typeId ? tokenMap.get(to.typeId) ?? undefined : undefined }, + })), + }, + })), + }, + } + } + + @Get('/:address/assets') + async accountAssets(@Param('address') address: string) { + const tokens = await this._dataSource.getRepository(Token).find() + const account = await this.getOrCreateAccount(address) + + const assets = await this._dataSource.getRepository(Asset).findBy({ accountId: account.id }) + const assetsMap = assets.reduce((acc, cur) => { + if (BI.from(cur.balance).gt(0)) { + acc.set(cur.tokenId, cur) + } + return acc + }, new Map()) + + 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) + } + }) + } +} 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)) - } -} diff --git a/packages/samples/sudt/src/controllers/sudt.controller.ts b/packages/samples/sudt/src/controllers/sudt.controller.ts index 408c0d18..7b694ae4 100644 --- a/packages/samples/sudt/src/controllers/sudt.controller.ts +++ b/packages/samples/sudt/src/controllers/sudt.controller.ts @@ -1,14 +1,31 @@ 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' +import { ExplorerService } from '../services/explorer.service' +import { BI, utils } from '@ckb-lumos/lumos' +import { MintRequest, TransferRequest } from '../dto/mint.dto' +import { LockModel } from '../actors/lock.model' -@Controller('sudt') +@Controller('token') export default class SudtController extends BaseController { + #explorerHost = process.env.EXPLORER_HOST || 'https://explorer.nervos.org' + constructor( + private _dataSource: DataSource, + private _explorerService: ExplorerService, + ) { + super() + } + @Get('/meta/:typeArgs') async meta(@Param('typeArgs') typeArgs: string) { if (!typeArgs) { @@ -20,31 +37,49 @@ 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('/send/:typeId') + 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') } - const sudtModel = appRegistry.findOrBind(new ActorReference('sudt', `/${typeArgs}/`)) + 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)) - return SudtResponse.ok(sudtModel.getSudtBalance(addresses.map((v) => getLock(v)))) + const sudtModel = appRegistry.findOrBind( + new ActorReference('sudt', `/${token.args}/${fromLocks[0].args}/`), + ) + + const lockModel = LockModel.getLock(from[0]) + + const result = sudtModel.send(lockModel, getLock(to), amount) + return SudtResponse.ok(await Tx.toJsonString(result)) } - @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('/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 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 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)) } @@ -61,4 +96,112 @@ export default class SudtController extends BaseController { ) return SudtResponse.ok(await Tx.toJsonString(result)) } + + @Post('/') + 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 amount = BI.from(req.amount) + 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() + + return new SudtResponse('201', await Tx.toJsonString(result)) + } 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) + return SudtResponse.err('500', { message: (e as Error).message }) + } + } + + @Put('/: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' }) + } + + this._explorerService.updateSUDT({ + typeHash: typeId, + symbol: req.name, + decimal: req.decimal.toString(), + totalAmount: '0', + description: req.description, + operatorWebsite: req.website, + }) + + try { + await this._dataSource.getRepository(Token).save({ ...token, ...req }) + return new SudtResponse('201', {}) + } catch (e) { + throw SudtResponse.err('500', { message: (e as Error).message }) + } + } + + @Get('/:typeId') + async getToken(@Param('typeId') typeId: string) { + const token = await this._dataSource.getRepository(Token).findOneBy({ typeId }) + + if (token) { + 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() + } + } + + @Get('/') + async listTokens() { + const tokens = await this._dataSource.getRepository(Token).find() + + 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/create-token.dto.ts b/packages/samples/sudt/src/dto/create-token.dto.ts new file mode 100644 index 00000000..ec8ee3b8 --- /dev/null +++ b/packages/samples/sudt/src/dto/create-token.dto.ts @@ -0,0 +1,15 @@ +export interface CreateTokenRequest { + name: string + account: string + decimal: number + description: string + website: string + icon: string + amount: string + email: string + explorerCode?: string +} + +export interface CreateTokenResponse { + url: string +} 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..6090b033 --- /dev/null +++ b/packages/samples/sudt/src/dto/mint.dto.ts @@ -0,0 +1,14 @@ +import { TransactionSkeletonType } from '@ckb-lumos/helpers' +import { HexString } from '@ckb-lumos/lumos' + +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/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/token.dto.ts b/packages/samples/sudt/src/dto/token.dto.ts new file mode 100644 index 00000000..a34c36d5 --- /dev/null +++ b/packages/samples/sudt/src/dto/token.dto.ts @@ -0,0 +1,29 @@ +import { Token } from '../entities/token.entity' + +export interface TokenResponse { + symbol: string + typeId: string + name: string + amount: string + decimal: number + description: string + website: string + icon: string + explorerUrl: string + owner: string +} + +export const tokenEntityToDto = (token: Token, owner: string, 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, + typeId: token.typeId, + explorerUrl: `${explorerHost}/sudt/${token.typeId}`, + owner, + } +} 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/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/entities/token.entity.ts b/packages/samples/sudt/src/entities/token.entity.ts new file mode 100644 index 00000000..15678642 --- /dev/null +++ b/packages/samples/sudt/src/entities/token.entity.ts @@ -0,0 +1,51 @@ +import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn, Unique, UpdateDateColumn } from 'typeorm' + +export enum TokenStatus { + New = 1, + Committed, +} + +@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 + + @Column({ default: TokenStatus.New }) + status!: TokenStatus + + @CreateDateColumn() + createdAt!: Date + + @UpdateDateColumn() + updatedAt!: Date +} 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 +} diff --git a/packages/samples/sudt/src/main.ts b/packages/samples/sudt/src/main.ts index 7bcba32f..2895401f 100644 --- a/packages/samples/sudt/src/main.ts +++ b/packages/samples/sudt/src/main.ts @@ -1,78 +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 OmnilockController from './controllers/omnilock.controller'; -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 './type-extends' +import 'dotenv/config' +import { bootstrap } from './bootstrap' -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()); - - // init kuai io - const cor = new CoR(); - const omnilockController = new OmnilockController(); - const sudtController = new SudtController(); - cor.use(omnilockController.middleware()); - cor.use(sudtController.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}`); - }); -} - -bootstrap(); +bootstrap() 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..06dd3d26 --- /dev/null +++ b/packages/samples/sudt/src/services/explorer.service.ts @@ -0,0 +1,45 @@ +export class ExplorerService { + constructor( + private host = 'https://testnet-api.explorer.nervos.org', + private _email: string, + ) {} + + updateSUDT = async (params: { + typeHash: string + symbol: string + decimal: string + totalAmount: string + description: string + 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`, + }) + console.log(`${this.host}/api/v1/udts/${params.typeHash}`) + const res = await fetch(`${this.host}/api/v1/udts/${params.typeHash}`, { + method: 'PUT', + body, + headers: { + Accept: 'application/vnd.api+json', + 'Content-Type': 'application/vnd.api+json', + }, + }) + + switch (Math.ceil(res.status / 100)) { + case 2: + case 3: + return true + case 4: + case 5: + throw new Error(`${res.status}, ${res.statusText}`) + } + } +} 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..aa250abe --- /dev/null +++ b/packages/samples/sudt/src/services/nervos.service.ts @@ -0,0 +1,98 @@ +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 + typeId?: string + ckb: string + amount?: string +} + +export class NervosService { + #indexer: Indexer + #rpc: RPC + constructor(rpcUrl: string, indexerUrl: string) { + this.#indexer = new Indexer(rpcUrl, indexerUrl) + this.#rpc = new RPC(rpcUrl) + } + + #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 typeId = utils.computeScriptHash(previousOutput.type) + if (typeIds.find((i) => typeId === i)) { + const previousOutputData = previousTransaction.transaction.outputsData[txIndex] + from.push({ + address: encodeToAddress(previousOutput.lock), + typeId, + ckb: previousOutput.capacity, + amount: BI.from(number.Uint128LE.unpack(previousOutputData.slice(0, 34))).toString(), + }) + } + } + } + + return from + } + + #filterTo = async (tx: Transaction, typeIds: string[]): Promise => + tx.outputs.reduce((acc, cur, key) => { + if (cur.type) { + const typeId = utils.computeScriptHash(cur.type) + if (typeIds.find((i) => typeId === i)) { + acc.push({ + address: encodeToAddress(cur.lock), + typeId, + ckb: cur.capacity, + amount: BI.from(number.Uint128LE.unpack(tx.outputsData[key].slice(0, 34))).toString(), + }) + } + } + return acc + }, []) + + 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) + const from = await this.#filterFrom(transaction, typeIds) + const to = await this.#filterTo(transaction, typeIds) + + return { + txHash, + from, + to, + } + }), + ) + + return { + lastCursor: txs.lastCursor, + history: history.filter((tx) => tx.from.length > 0 || tx.to.length > 0), + } + } +} 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..80a22e89 --- /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.sudtBalance.toString(), + }) + } else { + asset.setBalance(balance.sudtBalance) + } + + await this.#assetRepo.save(asset) + } + } catch (e) { + console.error(e) + } + } + await scheduler.wait(1000) + } + } +} 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..717aa28b --- /dev/null +++ b/packages/samples/sudt/src/tasks/token.task.ts @@ -0,0 +1,47 @@ +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, + decimal: token.decimal.toString(), + totalAmount: '0', + description: token.description ?? '', + operatorWebsite: token.website, + }) + + token.status = TokenStatus.Committed + await this.#tokenRepo.save(token) + } catch (e) { + console.error(e) + } + } + } + } + } +} 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 } } diff --git a/packages/samples/sudt/src/views/tx.view.ts b/packages/samples/sudt/src/views/tx.view.ts index 32e444ce..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, 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 { getConfig } from '@ckb-lumos/config-manager' +import { addBuiltInCellDeps } from '@ckb-js/kuai-common' +import { SudtResponse } from '../response' export class Tx { static async toJsonString({ @@ -14,55 +14,27 @@ export class Tx { witnesses?: string[] }): Promise { let txSkeleton = helpers.TransactionSkeleton({}) + for (const input of inputs) { + switch (input.cellOutput.lock.codeHash) { + 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 SudtResponse.err('400', 'not support lock script') + } + 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) } 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