智能合约编辑器杂谈

  1. 项目简介

  2. 智能合约简介

  3. git message规范

  4. 前端设计

  5. some troubles 

项目简介

  1. 属于开发者生态的一部分

  2. 编写、编译、部署、调用合约

  3. 面向开发者

  4. 暂时只支持c++

智能合约简介

智能合约(英语:Smart contract )是一种旨在以信息化方式传播、验证或执行合同的计算机协议。智能合约允许在没有第三方的情况下进行可信交易。这些交易可追踪且不可逆转。智能合约概念于1994年由Nick Szabo首次提出

智能合约简介

我理解的智能合约就是:

 在区块链上运行的不可篡改的代码

智能合约简介

传统合同:执行来自于相关方,约束来自于法律

智能合约:执行来自于代码,约束来自于代码(代码逻辑)和区块链(不可篡改)

区块链与传统合同的区别

智能合约简介

智能合约可以替代传统合同吗?

智能合约简介

智能合约可以做什么事情?

薪酬管理系统

setEmployeeBonus(employee,years,title,contribute,duration)

// [{name:’liuzongyuan’,amount:10000}]
queryBonus(employee)
withdrawBonus(amount)

自动转账

智能合约简介

智能合约适合做什么事情?

对不可篡改性有要求的事(比如承诺)

数字资产操作(奖金发放)

溯源相关(因为操作都有记录)

赌博(f3d p3d)

...

智能合约简介

智能合约的特点

  1. 不可篡改
  2. 执行失败会回滚
  3. 调用需要消耗手续费 

智能合约简介

有哪些限制?

  1. 不能通过发送网络请求读取外部数据
  2. 调用需要消耗手续费

智能合约简介

我们的合约跟以太坊有什么区别?

ethereum gxc
语言 solidity c++
手续费是否固定

智能合约简介

前端与智能合约的交互?

jsonrpc websocket

智能合约简介

合约代码展示

智能合约简介

疑问

序列化为什么是前端来做?

Git Messages

<type>(<scope>): <subject>

<BLANK LINE>

<body>

<BLANK LINE>

<footer>

Git Messages

<type>(<scope>): <subject>

<BLANK LINE>

<body>

<BLANK LINE>

<footer>

Type:改动类型
* feat (feature)
* fix (bug fix)
* docs (documentation)
* style (formatting, missing semi colons, …)
* refactor
* test (when adding missing tests)
* chore (maintain)

Scope:改动的地方

Subject:
* 用祈使语气: “change” ,而不是 “changed” 或 “changes”
* 首字母不大写
* 最后不要加.号

Body:
* 用祈使语气: “change” ,而不是 “changed” 或 “changes”
* 如果是改动,写明改动的动机以及和之前的区别

Footer:
1.Breaking changes
所有的Breaking changes都必须在footer声明改动点,改动的原因和迁移指南
BREAKING CHANGE: isolate scope bindings definition has changed and
    the inject option for the directive controller injection was removed.
    
    To migrate the code follow the example below:
    
    Before:
    
    scope: {
      myAttr: 'attribute',
      myBind: 'bind',
      myExpression: 'expression',
      myEval: 'evaluate',
      myAccessor: 'accessor'
    }
    
    After:
    
    scope: {
      myAttr: '@',
      myBind: '@',
      myExpression: '&',
      // myEval - usually not useful, but in cases where the expression is assignable, you can use '='
      myAccessor: '=' // in directive's template change myAccessor() to myAccessor
    }
    
    The removed `inject` wasn't generaly useful for directives so there should be no code using it.


2.Referencing issues
关闭的issue应该放到这里
Closes #123, #245, #992

前端设计

  1. 整体架构

  2. 文件目录设计

  3. 模板导入设计

  4. 合约列表设计

整体架构

├── .babelrc
├── .electron-vue
│   ├── build.js    // 构建客户端的脚本
│   ├── dev-client.js
│   ├── dev-runner.js    // npm run dev的script
│   ├── webpack.main.config.js    // 主进程配置
│   ├── webpack.renderer.config.js    // 渲染进程配置
│   └── webpack.web.config.js
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .travis.yml
├── README.md
├── appveyor.yml
├── package.json
├── postcss.config.js
├── src
│   ├── index.ejs
│   ├── main    // 主进程
│   │   ├── index.dev.js
│   │   ├── index.js    // 主进程入口
│   │   └── util
│   └── renderer    // 渲染进程
│       ├── App.vue    // 入口组件
│       ├── assets    // 图片、样式、字体等资源
│       ├── components    // vue组件
│       ├── const    // 常量(常量及公用的校验rule)
│       ├── filters    // vue 过滤器
│       ├── locales    // i18n
│       ├── main.js    // 入口文件
│       ├── plugins    // vue 插件
│       ├── router    // vue router
│       ├── services    // 与公信链或者服务端交互的逻辑
│       ├── store    // vuex
│       ├── template    // 工程模板,应该迁移到static
│       └── util
├── static    // 放变动较少的文件
│   └── .gitkeep
├── test
│   ├── .eslintrc
│   ├── e2e
│   │   ├── index.js
│   │   ├── specs
│   │   └── utils.js
│   └── unit
│       ├── data
│       ├── index.js
│       ├── karma.conf.js
│       └── specs
├── yarn-error.log
└── yarn.lock

脚手架:electron-vue

mvvm框架:vue

组件库:iview,最近升级到3.0

打包工具:webpack

文件目录设计

演示

文件目录设计

// item,单项数据
{
    title: 'file name',
    id: 'file ID',
    content: 'file content',
    isDirectory: true,
    expand: false,    // whether expanded
    children: []    // only directory has children
}
// 存放文件目录对应的数据,是一颗树
files: {
    id: 1,
    isRoot: true,
    isDirectory: true,
    title: 'root',
    children: [item, item, item]
}


// 存放code panel对应的数据,是一个数组
openedFiles: [item, item, item]

文件目录设计

vuex modules

return function(store) {
    const savedState = shvl.get(options, 'getState', getState)(key, storage);

    if (typeof savedState === 'object' && savedState !== null) {
      // 用本地的数据替换当前store的数据
      store.replaceState(merge(store.state, savedState, {
        arrayMerge: options.arrayMerger || function (store, saved) { return saved },
        clone: false,
      }));
    }

    // 在每个mutation操作时,都会执行该回调
    (options.subscriber || subscriber)(store)(function(mutation, state) {
      if ((options.filter || filter)(mutation)) {
        // 将当前store存到本地
        (options.setState || setState)(
          key,
          (options.reducer || reducer)(state, options.paths || []),
          storage
        );
      }
    });
  };

function setState(key, state, storage) {
    return storage.setItem(key, JSON.stringify(state));
  }

文件目录设计

首先需要扩展

{
    title: 'file name',
    ...
    render: function(h){
        return h('div',{...},[])
    }
}

其次需要deep clone

computed: {
    data() {
        var data = this.filterData(cloneDeep(this.files.children))
        return data
    }
}

文件目录设计

对数据进行操作,反馈到视图上

directoryMenu.append(new MenuItem({
    label: this.$t('files.addFile'),
    click: () => {
        this.appendFile({target: data})
    }
}))

// action
appendFile({commit}, payload) {
    commit('APPEND_FILE', payload)
}

// mutation
APPEND_FILE(state, {target, opts = {}}) {
    let tempNode = new TreeModel()
    tempNode = tempNode.parse(util.formatFile(opts))
    if (target.isDirectory) {
        filesTreeModel.first(idEq(target.id)).addChild(tempNode)
    } else {
        filesTreeModel.first(idEq(target.id)).parent.addChild(tempNode)
    }
}
watch: {
    data: {
        deep: true,
        handler () {
            this.stateTree = this.data;
            this.flatState = this.compileFlatState();
            this.rebuildTree();
        }
    }
}

文件目录设计

引入tree model,在mutation中使用,简化树操作

// good example, no need for deep clone

// init
afterInit(store) {
    filesTreeModel = new TreeModel()
    filesTreeModel = filesTreeModel.parse(store.state.ContractFiles.files)
}

// action
appendFile({commit}, payload) {
    commit('APPEND_FILE', payload)
}

// mutation
APPEND_FILE(state, {target, opts = {}}) {
    let tempNode = new TreeModel()
    tempNode = tempNode.parse(util.formatFile(opts))
    if (target.isDirectory) {
        filesTreeModel.first(idEq(target.id)).addChild(tempNode)
    } else {
        filesTreeModel.first(idEq(target.id)).parent.addChild(tempNode)
    }
}
// bad example, use tree model on action

// init tree model
afterInit(store) {
    filesTreeModel = new TreeModel()
    // 必须克隆,因为model引用store,否则在action中修改tree model会报错
    filesTreeModel = filesTreeModel.parse(cloneDeep(store.state.ContractFiles.files))
}

// action
appendFile({commit}, {target, opts = {}}) {
    let tempNode = new TreeModel()
    tempNode = tempNode.parse(util.formatFile(opts))
    if (target.isDirectory) {
        filesTreeModel.first(idEq(target.id)).addChild(tempNode)
    } else {
        filesTreeModel.first(idEq(target.id)).parent.addChild(tempNode)
    }
    commit('REFRESH_FILES')
}

// mutation
REFRESH_FILES(state) {
    // 必须克隆,否则model会被绑定,在action中修改tree model会报错
    state.files = cloneDeep(filesTreeModel.model)
}

文件目录设计

不要直接对state进行操作

// correct

// modules
export default {
    namespaced: true,
    state,
    mutations,
    actions,
    getters,
    util,
    afterInit(store) {
        filesTreeModel = new TreeModel()
        filesTreeModel = filesTreeModel.parse(store.state.ContractFiles.files)
    }
}

// vue store entry
const store = new Vuex.Store({
    state,
    mutations,
    getters,
    actions,
    plugins: [createPersistedState()],
    modules,
    strict: process.env.NODE_ENV !== 'production'
})

for (let key in modules) {
    modules[key].afterInit && modules[key].afterInit(store)
}
// wrong

const state = {
    files: []
}

let filesTreeModel = new TreeModel()
// 这里的state只是一个pure object,不会绑定数据,而且这里也没法拿到最终的数据
filesTreeModel = filesTreeModel.parse(cloneDeep(state.files))

文件目录设计

codepanel中修改代码,记得同时修改files

changeFileContent({commit, dispatch}, {target, content} = {}) {
    // 修改opened files中的content
    commit('CHANGE_CURRENT_OPENED_FILE_CONTENT', {node: target, content})
    // 修改files中的content
    dispatch('changeFileStatus', {node: target, opts: {content}})
}

模板导入设计

交互演示

模板导入设计

希望每个模板工程,就是一个文件夹

// https://webpack.js.org/guides/dependency-management/
// 这是webpack提供的能力,会把文件目录单独打包成一个模块,支持打包子目录
const helloTpl = require.context('@/template/hello', true, /\.ejs$/)

helloTpl.keys().map(key => {
    return {
        title: filter(key),
        content: helloTpl(key)
    }
})

模板导入设计

希望在运行时根据传入的参数进行渲染

const helloTpl = require.context('@/template/hello', true, /\.ejs$/)

helloTpl.keys().map(key => {
    return {
        title: filter(key),
        // 这里每个content拿到的并不是plain text,而是编译好等待渲染的模板
        content: helloTpl(key)
    }
})

// 渲染
content({renderOpts})
// webpack,ejs-loader并没有用ejs官方的engine,而是用loadsh的template方法作为engine
rules: [
    { test: /\.ejs$/, loader: 'ejs-loader' }
]

模板导入设计

选择模板

编译

渲染

添加到文件树

=>

=>

=>

const metaFiles = require.context('@/template', true, /meta\.js$/)
// 主要是生成文件树所需要的结构,目前gxcpp暂时不支持嵌套结构的编译,
// 所以最终生成的children中是平级的结构
util.compile = function (directory) {
    let files = tplMap[`${directory}Tpl`]
    const reg = /^\.\/(.+)\.ejs$/
    files = files.keys().map(key => {
        return {
            title: reg.exec(key)[1],
            content: files(key)
        }
    })

    const ret = {
        title: directory,
        children: files
    }

    ret.render = util.render.bind(util, ret)

    return ret
}
// 基于编译生成的数据结构,去渲染content,然后返回文件树所需的数据结构
util.render = function (project, opts = {}) {
    opts.entry = opts.entry || metaMap[project.title].entry
    if (opts.title) {
        project.title = opts.title
    }

    project.children = project.children.map(file => {
        // every child file render
        file.content = file.content(opts)
        return file
    })

    delete project.render

    return project
}
// action
addProject({commit}, project) {
    commit('ADD_PROJECT', project)
}

// mutation
ADD_PROJECT(state, project) {
    let tempNode = new TreeModel()
    tempNode = tempNode.parse(util.formatFiles(project))
    filesTreeModel.addChild(tempNode)
}

合约列表设计

演示

合约列表设计

合约列表设计

部署

表单渲染

表单数据收集

数据处理

=>

=>

=>

数据序列化

调用

=>

=>

const deploy_contract = ({from = '', contractName = '', code = '', 
    abi = '', fee_id = '', password = '', broadcast = true}) => {
    ...
    return new Promise((resolve, reject) => {
        // 1.解锁钱包,本质上是根据password和账户信息,经过一系列密码学运算得到私钥
        resolve(Promise.all([fetch_account(from), 
            unlock_wallet(from, password)]).then(results => {
            ...
            // 2.构造交易,会去
            let tr = new TransactionBuilder()
            tr.add_operation(tr.get_type_operation('create_contract', {
                'fee': {
                    'amount': 0,
                    'asset_id': fee_id
                },
                'name': contractName,
                'account': fromAcc.id,
                vm_type,
                vm_version,
                code,
                abi
            }))

            // 3.处理交易
            return process_transaction(tr, from, password, broadcast).then((resp) => {
                // ... handle resp
                return resp
            })
        }))
    })
}
const process_transaction = (tr, account, password, broadcast) => {
    let walletInfo = null
    return new Promise((resolve, reject) => {
        // 1.解锁钱包
        resolve(unlock_wallet(account, password).then(info => {
            walletInfo = info
            // 2.设置手续费
            return Promise.all([tr.update_head_block(), 
                tr.set_required_fees()]).then(() => {
                // 3.用私钥签名
                tr.add_signer(PrivateKey.fromWif(walletInfo.wifKey))
                if (broadcast) {
                    // 4.广播交易
                    return tr.broadcast()
                } else {
                    return tr
                }
            })
        }))
    })
}
deploy_contract({
    from: this.currentWallet.account,
    fee_id: this.tempAsset.id,
    password: this.tempPwd,
    broadcast: true,
    abi: this.abi,
    contractName: this.contractName,
    code: this.bytecode
}).then((resp) => {
    ...
    // 将部署过的合约保存在本地
    this.appendContract(resp[0].ext)
    this.$store.dispatch('updateCurrentBalancesAndAssets')
})
return process_transaction(tr, from, password, broadcast).then((resp) => {
    return fetch_account(contractName).then((account) => {
        resp[0].ext = {
            abi,
            from,
            contractName,
            contractId: account.id,
            fee: resp[0].trx.operations[0][1].fee
        }

        return resp
    })
})

deploy_contract({
    ...
}).then((resp) => {
    ...
    // 将部署过的合约保存在本地
    this.appendContract(resp[0].ext)
    this.$store.dispatch('updateCurrentBalancesAndAssets')
})

合约列表设计

部署

表单渲染

表单数据收集

数据处理

=>

=>

=>

数据序列化

调用

=>

=>

computed: {
    ...mapState('ContractOperation', {
        contracts: state => {
            return contractsFilter(cloneDeep(state.contracts))
        }
    })
}

function contractFilter(contract) {
    contract.functions = contract.abi.actions.map((action) => {
        var struct = contract.abi.structs.find((struct) => {
            return struct.name === action.name
        })
        return {
            ...struct,
            ...action
        }
    })
    return contract
}

<div class="contract" v-for="contract in contracts">
	…
        <function-card v-for="f in contract.functions" :payable="f.payable"
             :abi="contract.abi" :contractName="contract.contractName"
                   :name="f.name" :fields="f.fields"></function-card>
</div>

合约列表设计

部署

表单渲染

表单数据收集

数据处理

=>

=>

=>

数据序列化

调用

=>

=>

合约列表设计

部署

表单渲染

表单数据收集

数据处理

=>

=>

=>

数据序列化

调用

=>

=>

/* functionCard.vue */
// 也是层层向上,构建类似这样一个数组,[{name:'xx', type:'xx', value: xx}, ...]
var fields = this.$refs.fields.getFields()
params = await fieldUtil.formatFields2Params(fields)

/* fieldUtil.js */
util.formatFields2Params = async function (fields) {
    var ret = {}
    for (const field of fields) {
        if (util.isArrayType(field.type)) {    // 比如asset[]
            ret[field.name] = await Promise.all(field.value.map(val => {
                return util.formatField(util.getSingleType(field.type), val)
            }))
        } else {
            ret[field.name] = await util.formatField(field.type, field.value)
        }
    }
    return ret
}

util.formatField = async function (type, value) {
    let handler = handlers[type]
    if (!handler) {
        handler = handlers.defaultHandler
    }

    value = await handler(value)
    return value
}

合约列表设计

部署

表单渲染

表单数据收集

数据处理

=>

=>

=>

数据序列化

调用

=>

=>

// 上一步得到的处理后的参数数据
params = await fieldUtil.formatFields2Params(fields)

// 序列化参数
data = serializer.serializeCallData(this.name, params, this.abi).toString('hex')
const serializeCallData = (action, params, abi) => {
    abi = cloneDeep(abi)
    let struct = abi.structs.find(s => s.name === action)
    let b = new ByteBuffer(ByteBuffer.DEFAULT_CAPACITY, ByteBuffer.LITTLE_ENDIAN)
    struct.fields.forEach(f => {
        let value = params[f.name]
        let isArrayFlag = false
        if (isArrayType(f.type)) {
            isArrayFlag = true
            f.type = f.type.split('[')[0]
        }

        let type = types[f.type]
        if (!type) {
            let t = abi.types.find(t => t.new_type_name === f.type)
            if (t) {
                type = types[t.type]
            }
        }
        if (!type) {
            type = ops[f.type]
        }

        if (type) {
            if (isArrayFlag) {
                type = types.set(type)
            }
            type.appendByteBuffer(b, value)
        }
    })
    return Buffer.from(b.copy(0, b.offset).toBinary(), 'binary')
}

合约列表设计

部署

表单渲染

表单数据收集

数据处理

=>

=>

=>

数据序列化

调用

=>

=>

/* FunctionCard.vue */
call_contract(this.currentWallet.account, this.contractName, {
    'method_name': this.name,
    'data': data
}, this.tempAsset.id, this.tempPwd, true, this.amount)

/* WalletService.js */
const call_contract = (from, target, act, fee_id, password, broadcast = true, amount = {}) => {
    return new Promise((resolve, reject) => {
        resolve(Promise.all([fetch_account(from), fetch_account(target), 
            unlock_wallet(from, password)]).then(results => {
            ...
            let tr = new TransactionBuilder()
            let opts = {
                'fee': {
                    'amount': 0,
                    'asset_id': fee_id
                },
                'account': fromAcc.id,
                'contract_id': contractAccount.id,
                'method_name': act.method_name,
                'data': act.data
            }
            
            ...
            tr.add_operation(tr.get_type_operation('call_contract', opts))
            return process_transaction(tr, from, password, broadcast)
        }))
    })
}

Some Troubles

  1. 循环依赖
  2. serializer
  3. iview modal
  4. render
  5. sprites
  6. icons生成
  7. Keep alive
  8. event bus
  9. info panel

Thank you!

gxb打赏账户:jaredliu233

智能合约编辑器杂谈

By Jared Liu

智能合约编辑器杂谈

  • 448