Skip to content

Latest commit

 

History

History
330 lines (246 loc) · 15.5 KB

mvp-dapp.md

File metadata and controls

330 lines (246 loc) · 15.5 KB

Build a profile DApp with Kuai

This tutorial presents the fundamental steps to construct an MVP DApp utilizing the Kuai Framework. The codebase for the DApp is available at https://github.com/ckb-js/kuai/tree/develop/packages/samples/mvp-dapp.

This tutorial is specific to commits

1. Contract and Backend: https://github.com/ckb-js/kuai/tree/962d503e75dc808812ec216409877ddc8545c109
2. Frontend: https://github.com/Magickbase/kuai-mvp-dapp-ui/tree/97254673b545d3f2dc8edcd376f21c13d727c267

A user-oriented DApp typically requires the development of front-end, 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 for their profile information, and provide basic read-write APIs. Simple validation will be performed on the profile to demonstrate contract verification.

Before we start

A node.js CLI tool typically needs to be installed via npm before it can be utilized. 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 steps1 below to install kuai-cli locally

$ git clone [email protected]: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
├── contract
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── capsule.toml
│   ├── deployment.toml
├── 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/ is the workspace for contract development.

Contract

* The complete source code of demo can be found at https://github.com/ckb-js/kuai/tree/962d503e75dc808812ec216409877ddc8545c109/packages/samples/mvp-dapp/contract
* Go through contract development on YouTube

Contract workspace

The contract workspace locates at profile/contract, where the development, testing, and deployment will take place.

* The contract workspace will be polished in the future once the best practice of contract module structure is confirmed.(nervosnetwork/capsule#124)
* capsule2 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 is ready on your machine.

Add contract source code

* This step requires docker

# 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 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-cli3, 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 Faucet4 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-cli5

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

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

Deploy contracts by tx file

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/962d503e75dc808812ec216409877ddc8545c109/packages/samples/mvp-dapp/contract/deployed_demo

Backend

* The backend source code of demo can be found at 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

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/' } })
@LockFilter()
@DataFilter('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.newWithFilter(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. LockFilter indicates that OmnilockModel follows a pattern of lock script;
  3. DataFilter 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

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/' } })
@LockFilter()
@TypeFilter()
@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.newWithFilter(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 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:

Footnotes

  1. Kuai Installation: https://github.com/ckb-js/kuai#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: kuai-cli-deploy-contract.md