ckb-js / kuai

A protocol and framework for building universal dapps on Nervos CKB
MIT License
22 stars 11 forks source link

Tutorial to build a profile DApp with Kuai #306

Closed Keith-CY closed 1 year ago

Keith-CY commented 1 year ago

Build a profile DApp with Kuai

This tutorial is specific to commits 1. Contract and Backend: https://github.com/ckb-js/kuai/tree/ebffba1bc73255fa3ccf6d85300129c70bf88f47 2. Frontend: https://github.com/Magickbase/kuai-mvp-dapp-ui/tree/97254673b545d3f2dc8edcd376f21c13d727c267

A user-oriented DApp typically requires the development of frontend, backend, and smart contracts. As Kuai is a contract and server development framework specific for Nervos CKB, this article will not cover tutorials on front-end development.

This article describes how to develop a Profile Application using Kuai. The application will collect users' blank Omnilock cells as storage space, write their profile information, and provide basic read-write APIs. Simple validation will be performed on the profile to demonstrate contract verification.

Before we begin

A node.js CLI tool typically needs to be installed via npm before it can be used. Since Kuai is still in the development phase and has not been published to the npm registry, we need to clone Kuai Repository to our local machine and use npm link to install kuai-cli.

Follow the steps[^1] below to install kuai-cli locally

$ git clone git@github.com:ckb-js/kuai.git

$ cd kuai

$ npm install

$ npm run build

$ cd packages/cli

$ npm link

Initialize the project

# <your workspace> should be located in kuai repo because some dependencies are not published yet
# you may initialize the project at <kuai-repo>/packages/samples, "profile" in this tutorial
$ mkdir <kuai-repo>/packages/samples/profile

# cd <your workspace>
$ cd profile

$ kuai init
# samples git:(develop) kuai init
# 888    d8P                    d8b
# 888   d8P                     Y8P
# 888  d8P
# 888d88K     888  888  8888b.  888
# 8888888b    888  888     "88b 888
# 888  Y88b   888  888 .d888888 888
# 888   Y88b  Y88b 888 888  888 888
# 888    Y88b  "Y88888 "Y888888 888
#
# Welcome to kuai v0.0.1
#
# ✔ Kuai project root: · /kuai/packages/samples/profile
# ✔ Do you want to add a .gitignore? (Y/n) · true
# ✔ Do you want to build doc after create project? (Y/n) · true
#
# add kuai depende into package
#
# ......
#
# > doc
# > typedoc
#
# [info] Documentation generated at ./docs
# ✨ doc build success, you can see it at ./docs ✨
#
# ✨ Project created ✨
#
# See the README.md file for some example tasks you can run

The project template will be available as follows:

profile
├── Development.md
├── README.md
├── _cache
├── docs
│   ├── assets
│   ├── classes
│   ├── index.html
│   └── modules
├── jest.config.ts
├── kuai.config.ts
├── libs
├── node_modules
├── package.json
├── src
│   ├── actors
│   ├── app.controller.ts
│   ├── main.ts
│   └── type-extends.ts
├── tsconfig.json
└── typedoc.json

src/main.ts is the entry point of profile application. It registers services before the application starts; src/app.controller.ts is the router, which defines APIs exposed to users; src/actors/ is the directory to define actor models based on specific patterns. Learn more about actor models in Kuai;

Contract

The complete source code of demo can be found at https://github.com/yanguoyu/kuai/tree/b988eedbb224e15f5bb1d374e8d0345b8a558dc7/packages/samples/mvp-dapp/contract

Add contract module

The contract module will be delivered in project template in the future once the best practice of contract module structure is confirmed.(https://github.com/nervosnetwork/capsule/discussions/124) [capsule](https://github.com/nervosnetwork/capsule)[^2] is required for contract development. It will be installed on-demand automatically in the future. Now it should be installed manually by `cargo install ckb-capsule` if [rust](https://www.rust-lang.org/) is ready on your machine.

# in <kuai-repo>/packages/samples/profile
# capsule new <contract project name>
$ capsule new contract

After then

profile
└── contract
    ├── Cargo.toml
    ├── README.md
    ├── build
    ├── capsule.toml
    ├── contracts
    ├── deployment.toml
    └── migrations

is ready for contract development.

Add contract source code

This step requires [docker](https://www.docker.com/)
# kuai contract new --name <contract name>
$ kuai contract new --name kuai-mvp-contract

Contract template named kuai-mvp-contract will be generated in profile/contract/contracts

Fill the kuai-mvp-contract we've implemented into profile/contract/contracts/kuai-mvp-contract/src.

Contract Libs One unwritten norm is to abstract business logic into a lib([types](https://github.com/yanguoyu/kuai/tree/c1a25782234c95ea2ddd86449f07ca38e15559dc/packages/samples/mvp-dapp/contract/types)in our case), which relates to the skills of contract development and will not be elaborated here.

Build and deploy contract

# cd to /profile
$ kuai contract build --release

Contract artifacts will be generated in profile/contract/build/release/ for deployment.

The contract will be deployed by the default signer, ckb-cli[^3], in this case, so we need to create an account for signing transactions.

Creating an account in ckb-cli will be supported by kuai-cli in the future
# create an account in ckb-cli
$ ckb-cli account new --wait-for-sync

# Your new account is locked with a password. Please give a password. Do not forget this password.
# Password: ********
# Repeat password: ********
# address:
#   mainnet: ckb***************************************wqw7
#   testnet: ckt***************************************slzz
# lock_arg: 0x8d**********************************fbf4
# lock_hash: 0xabd*********************************************************fbaf

Go to CKB Testnet Faucet[^4] and claim 300,000 CKB for contract deployment.

# wait until your balance is correct
$ ckb-cli wallet get-capacity --address ckt***************************************slzz

# total: 300000.0 (CKB)

Deploy contracts with kuai-cli[^5]

# cd to /profile
# kuai contract deploy --name <contract name> --from <deployer address> --netwrok <chain type> --send --signer <signer>
$ kuai contract deploy --name kuai-mvp-contract --from ckt***************************************slzz --network testnet --send --signer ckb-cli

# [warn] `config` changed, regenerate lockScriptInfos!
# Input ckt1q*****************************************************************************************gt9's password for sign messge by ckb-cli:
# deploy success, txHash:  0x1ed********************************************************fbd3c

So far, we've completed the development&deployment of contracts.

The deployment information should be generated automatically as a facility for the backend. It will be implemented soon, now we have to set it manually at profile/contract/deployed/contracts.json.

The deployment information of the online demo can be found at https://github.com/ckb-js/kuai/tree/ebffba1bc73255fa3ccf6d85300129c70bf88f47/packages/samples/mvp-dapp/contract/deployed_demo

Backend

The backend source code of demo can be found at [samples/mvp-dapp](https://github.com/yanguoyu/kuai/tree/b988eedbb224e15f5bb1d374e8d0345b8a558dc7/packages/samples/mvp-dapp)

Actor Models

An actor model is an abstract of a bundle of cells matched by specific patterns. By defining an actor model in Kuai, cells will be collected and decoded automatically to read and write.

There're two actor models mapped from the cells:

  1. Omnilock Model: mapped from blank omnilock cells of a specific address as the original storage space;
  2. Record Model: mapped from cells that hold profile data, and constrained by the contract above.

These two models are the core of the entire backend application, and they are defined in profile/src/actors/

Omnilock Model
The source code of omnilock model can be found at [samples/mvp-dapp/src/actors/omnilock.model.ts](https://github.com/yanguoyu/kuai/blob/b988eedbb224e15f5bb1d374e8d0345b8a558dc7/packages/samples/mvp-dapp/src/actors/omnilock.model.ts)

At first, the built-in model named JSONStore should be imported as the basic model, and decorated by patterns as follows

@ActorProvider({ ref: { name: 'omnilock', path: '/:args/' } })
@LockPattern()
@DataPattern('0x')
@Omnilock()
export class OmnilockModel extends JSONStore<Record<string, never>> {
  constructor(
    @Param('args') args: string,
    _schemaOptions?: void,
    params?: {
      state?: Record<OutPointString, never>
      chainData?: Record<OutPointString, UpdateStorageValue>
      schemaPattern?: SchemaPattern
    }
  ) {
    super(undefined, { ...params, ref: ActorReference.newWithPattern(OmniLockModel, `/${args}`) })
    this.registerResourceBinding()
  }
}

Pay attention to the decorators above OmnilockModel

  1. ActorProvider defines how the model instance should be indexed. It works with the constructor parameter args to construct a unique index;
  2. LockPattern indicates that OmnilockModel follows a pattern of lock script;
  3. DataPattern make sure all cells collected are plain cells;
  4. Omnilock injects a well-known cell pattern for LockPattern.

By prepending all these decorators, an OmnilockModel instance represents cells owned by OmniLockAddress(args) as an entire object during runtime.

After then, we can add custom methods according to the business logic. Here we add meta to get capacity of the model and claim to transform plain omnilock cells into a profile-hold cell.

Record Model
The source code of record model can be found at [samples/mvp-dapp/src/actors/record.model.ts](https://github.com/yanguoyu/kuai/blob/b988eedbb224e15f5bb1d374e8d0345b8a558dc7/packages/samples/mvp-dapp/src/actors/record.model.ts)

Similarly, the RecordModel can be decorated to limit its cells by omnilock and type script of profile contract

@ActorProvider({ ref: { name: 'record', path: '/:codeHash/:hashType/:args/' } })
@LockPattern()
@TypePattern()
@Lock() // arbitrary lock pattern
@Type(PROFILE_TYPE_SCRIPT) // Inject PROFILE_TYPE_SCRIPT for Type Pattern. PROFILE_TYPE_SCRIPT can be imported from the facility generated by contract deployment
export class RecordModel extends JSONStore<{ data: { offset: number; schema: StoreType['data'] } }> { // offset can be removed along with data prefix pattern
  constructor(
    @Param('codeHash') codeHash: string, // inject lock script code hash for Lock Pattern
    @Param('hashType') hashType: string // inject lock script hash type for Lock Pattern
    @Param('args') args: string // inject lock script args for Lock Pattern
    _schemaOptions?: { data: {offset:number} }
    params?:{
      states?: Record<OutPointString, StoreType>
      chainData?: Record<OutPointString, UpdateStorageValue>
      cellPattern?: CellPattern
      schemaPattern?: SchemaPattern
    }
  ) {
    super(
      { data: { offset: (DAPP_DATA_PREFIX_LEN - 2) / 2 } },
      {
        ...params,
        ref: ActorReference.newWithPattern(RecordModel, `/${codeHash}/${hashType}/${args}/`),
      },
    )

    this.registerResourceBinding()
  }
}

Define update to change profile, and clear to wipe profile out

View

Notice that the responses of OmnilockModel and RecordModel consist of inputs, outputs, and cellDeps. A wrapper is required to transform them into a valid transaction. This step can be done anywhere. In this case, a view module is introduced to wrap them into a transaction.

Controller

Finally, requests from a client should be routed to the correct models; routes are defined in the generated app.controller.ts file. For instance,

// define a claim method to generate a transaction to transform blank omnilock cells into a profile-hold cell
router.post<never, { address: string }, { capacity: HexString }>('/claim/:address', async (ctx) => {
  const { body, params } = ctx.payload

  if (!body?.capacity) {
    throw new BadRequest('undefined body field: capacity')
  }

  const omniLockModel = appRegistry.findOrBind<OmnilockModel>( // get omnilock model instance
    new ActorReference('omnilock', `/${getLock(params?.address).args}/`)
  )
  const result = omniLockModel.claim(body.capacity) // get inputs and outputs
  ctx.ok(MvpResponse.ok(await Tx.toJsonString(result))) // transform inptus and outputs into a transaction by tx view
})

Chain Source

The [**Chain Source**](https://github.com/yanguoyu/kuai/blob/b988eedbb224e15f5bb1d374e8d0345b8a558dc7/packages/samples/mvp-dapp/src/chain-source.ts) module synchronizes data from CKB Node to Actor Models. It will be supported internally in the future and can be skipped in the code tour now.

In conclusion, the modules introduced above are the critical components of the entire backend application, as they constitute the overall logic of the entire application. Please visit samples/mvp-dapp to learn all the details.

Ref:

[^1]: Kuai Installation: https://github.com/ckb-js/kuai/blob/c2845169de81817fd2fd397032672f79bf73aebd/README.md#kuai-installation [^2]: Capsule and its prerequisites: https://github.com/nervosnetwork/capsule#prerequisites [^3]: CKB CLI: https://github.com/nervosnetwork/ckb-cli [^4]: CKB Testnet Faucet: https://faucet.nervos.org/ [^5]: Deploy contracts by kuai-cli: https://github.com/ckb-js/kuai/pull/242

Keith-CY commented 1 year ago

Here's the tutorial of samples/mvp-dapp with contract.

Please have a review @zhengjianhui @yanguoyu @Daryl-L @PainterPuppets

Ref: Add demonstration of how to integrate contract development within the mvp

zhengjianhui commented 1 year ago
$ yarn bootstrap

to perform a dependency installation first

zhengjianhui commented 1 year ago
yarn bootstrap
➜ yarn bootstrap
yarn run v1.22.10
error Command "bootstrap" not found.
yanguoyu commented 1 year ago

After lerna update to v7, only need to run yarn or npm i to replace yarn bootstrap. https://github.com/ckb-js/kuai/pull/310

zhengjianhui commented 1 year ago

更新到 v7后lerna,只需运行yarnnpm i替换yarn bootstrap. #310

ok

zhengjianhui commented 1 year ago
cd packages/cli

lerna notice cli v7.0.0
✔  @ckb-js/kuai-typeorm:build (3s)

✖  @ckb-js/kuai-common:build

$ tsc src/util.ts(2,26): error TS2307: Cannot find module '@ckb-lumos/lumos' or its corresponding type declarations. error Command failed with exit code 2.

Keith-CY commented 1 year ago
cd packages/cli
lerna notice cli v7.0.0

    ✔  @ckb-js/kuai-typeorm:build (3s)

    ✖  @ckb-js/kuai-common:build
$ tsc
       src/util.ts(2,26): error TS2307: Cannot find module '@ckb-lumos/lumos' or its corresponding type declarations.
error Command failed with exit code 2.

The dependency @ckb-lumos/lumos was a shadow dependency missing in kuai/packages/common/package.json, it's installed by lerna but ignored by npm/yarn

So @ckb-lumos/lumos should be added in kuai/packages/common/package.json

zhengjianhui commented 1 year ago
@ckb-lumos/lumos
$ npm install  @ckb-lumos/lumos --spoce @ckb-js/kuai-common
zhengjianhui commented 1 year ago

It is recommended to use a tag to fix the version

Keith-CY commented 1 year ago
@ckb-lumos/lumos
$ npm install  @ckb-lumos/lumos --spoce @ckb-js/kuai-common

It's fixed by https://github.com/ckb-js/kuai/pull/313

Keith-CY commented 1 year ago

It is recommended to use a tag to fix the version

Lerna is version locked at https://github.com/ckb-js/kuai/blob/develop/package.json#L30

The shadow dependencies are not installed because lerna is opted out during the installation.

yanguoyu commented 1 year ago

https://github.com/ckb-js/kuai/tree/develop/packages/samples/mvp-dapp#getting-started This document doesn't seem complete enough. We can add how to start dev and add docs on how to build dependence(kuai).

Keith-CY commented 1 year ago

develop/packages/samples/mvp-dapp#getting-started This document doesn't seem complete enough. We can add how to start dev and add docs on how to build dependence(kuai).

What if merging this document(https://github.com/ckb-js/kuai/issues/306) into https://github.com/ckb-js/kuai/tree/develop/docs and add a link in https://github.com/ckb-js/kuai/blob/develop/packages/samples/mvp-dapp/README.md with a concise description

yanguoyu commented 1 year ago

develop/packages/samples/mvp-dapp#getting-started This document doesn't seem complete enough. We can add how to start dev and add docs on how to build dependence(kuai).

What if merging this document(#306) into https://github.com/ckb-js/kuai/tree/develop/docs and add a link in https://github.com/ckb-js/kuai/blob/develop/packages/samples/mvp-dapp/README.md with a concise description

How about using https://github.com/ckb-js/kuai/pull/326 to improve it?

Keith-CY commented 1 year ago

develop/packages/samples/mvp-dapp#getting-started This document doesn't seem complete enough. We can add how to start dev and add docs on how to build dependence(kuai).

What if merging this document(#306) into develop/docs and add a link in develop/packages/samples/mvp-dapp/README.md with a concise description

How about using #326 to improve it?

It would be great

Keith-CY commented 1 year ago

Will be updated by https://github.com/ckb-js/kuai/pull/331