• 2020年7月14日 10:23

    RChain已经提供了两个API供开发者使用:
    1 Tomislav开发的Javascript版
    github.com/rchain/exchange-api-njs/

    2 Will开发的Python版
    github.com/rchain/pyrchain

    但是这两个API只适合于在Server端使用,如果希望从浏览器端发起与RChain节点的交互,可以参考我这个教程。

    视频在这里:www.bilibili.com/video/BV1Sv411i7mg/

    代码库:github.com/dimworm/RChain-API-demo

    下面是文字解释:

    以Reactjs为例,先创建一个项目:

    npx create-react-app rchain-api-demo
    cd rchain-api-demo
    npm start
    

    安装API

    首先安装官方Javascript版API:

    npm install --save-dev grpc-tools protobufjs
    npm install rchain/exchange-api-njs
    

    由于墙的原因,走到最后会失败,设置穿墙也于事无补。
    问题在于安装步骤最后会运行:

    rnode-grpc --gen-dir lib/rnode-grpc-gen --rnode-version v0.9.24
    

    正常情况下应该会生成包含一堆机器生成代码的rnode-grpc-gen 文件夹。解决办法是大家可以从我的代码库中找到这个文件夹,把他copy到自己的项目中即可。注意:上面的命令还是需要运行的,直至它报错退出为止。

    想要对整个过程有更详细了解的话,请见Tomislav的代码:
    github.com/tgrospic/rnode-grpc-js

    页面代码

    我们实现两个功能:
    1. 查询余额
    2. 转账

    查询余额,我们只需要一个输入框,一个按钮,点击按钮会触发checkBalance。
    转账,我们需要一个地址输入框,一个数量输入框和一个按钮。点击按钮会触发transfer。

    
      return (
        <div >
          <form>
            <input type="text" name="revAddress" value={state.revAddress} onChange={handleInputChange} />
            <br/><br/>
            <Button  variant="contained" color="primary" onClick={()=> checkBalance(state.revAddress)} >
              Check
            </Button>
            <br/><br/>
          </form>
          <form>
            <input type="text" name="toAddress" value={state.toAddress} onChange={handleInputChange} />
            <br/><br/>
            <input type="number" name="amount" value={state.amount} onChange={handleInputChange} />
            <br/><br/>
            <Button  variant="contained" color="primary" onClick={()=> transfer(state.revAddress, state.toAddress, state.amount)} >
              Transfer
            </Button>
          </form>
        </div>
      )
    

    导入库

    我们会用到API中的signDeploy功能,我们还需要引入rnode-grpc-gen中的两个文件。

    import React, {useState} from 'react'
    import Button from '@material-ui/core/Button'
    
    const { signDeploy } = require('@tgrospic/rnode-grpc-js')
    require('./rnode-grpc-gen/js/DeployServiceV1_pb')
    require('./rnode-grpc-gen/js/ProposeServiceV1_pb')
    

    节点

    我们会和两个节点进行交互,一个只读节点,一个验证节点。只读节点用于读取链上数据;验证节点用于出块,帮助我们完成转账等功能。

    const readonly = `https://observer-asia.services.mainnet.rchain.coop`
    const validator = `https://node5.root-shard.mainnet.rchain.coop`
    

    账户查询

    替换掉Rholang代码中的钱包地址,然后通过http向只读节点POST这段Rholang代码,返回结果就是账户余额。注意在链上的余额需要除以10的8次方,才是我们的真实账户余额。

    const checkBalance = async (revAddress) => {
      const req = {
        method: 'POST',
        body: `
                new return, rl(\`rho:registry:lookup\`), RevVaultCh, vaultCh in {
                  rl!(\`rho:rchain:revVault\`, *RevVaultCh) |
                  for (@(_, RevVault) <- RevVaultCh) {
                    @RevVault!("findOrCreate", "${revAddress}", *vaultCh) |
                    for (@maybeVault <- vaultCh) {
                      match maybeVault {
                        (true, vault) => @vault!("balance", *return)
                        (false, err)  => return!(err)
                      }
                    }
                  }
                }
              `
      }
    
      const  balance= await fetch(readonly+`/api/explore-deploy`, req).then(r => r.json()).then(x =>  x.expr[0].ExprInt.data)
      console.log(balance)
    
    }
    

    转账

    1. privateKey要用安全的方式保存,不能写到代码里,这里只是证明,不是实际代码。
    2. 在链上部署代码时,需指定一个区块区间,过了这个区间,部署自动失效。所以我们先查询一下lastBlock,把lastBlock赋值给validafterblocknumber。系统会在这个区块及随后的50个区块构成的区间进行部署。
    3. phlolimit和phloprice对应于以太坊的gaslimit和gasprice。
    4. phlolimit 我们用300K,这个值也可以选用小一些的值。节点会将没用完的部分返还。
    const  transfer = async (revAddress, toAddress, amount) => { 
      const privateKey = 'XXXX'
      const code =`
        new rl(\`rho:registry:lookup\`), RevVaultCh, vaultCh, toVaultCh, deployerId(\`rho:rchain:deployerId\`), revVaultKeyCh, resultCh in {
          rl!(\`rho:rchain:revVault\`, *RevVaultCh) |
          for (@(_, RevVault) <- RevVaultCh) {
            @RevVault!("findOrCreate", "${revAddress}", *vaultCh) |
            @RevVault!("findOrCreate", "${toAddress}", *toVaultCh) |
            @RevVault!("deployerAuthKey", *deployerId, *revVaultKeyCh) |
            for (@(true, vault) <- vaultCh; key <- revVaultKeyCh; @(true, toVault) <- toVaultCh) {
              @vault!("transfer", "${toAddress}", "${amount}", *key, *resultCh) |
              for (_ <- resultCh) { Nil }
            }
          }
        }
      `
    
      const lastBlock = await fetch(readonly + `/api/last-finalized-block`, { method:'get' })
          .then((str) => str.json()).then( x =>  x.blockInfo.blockNumber )
    
      const deployData = {
        term: code, phlolimit: 300000, phloprice: 1,
        validafterblocknumber: lastBlock
      }
      const deploy = await signDeploy(privateKey, deployData)
      const da = {
        data: {
          term: deploy.term,
          timestamp: deploy.timestamp,
          phloPrice: deploy.phloprice,
          phloLimit: deploy.phlolimit,
          validAfterBlockNumber: deploy.validafterblocknumber
        },
        sigAlgorithm: deploy.sigalgorithm,
        signature: bytesToHex(deploy.sig),
        deployer: bytesToHex(deploy.deployer)
      }
      const req = {
        method: 'POST',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(da)
      }
      const b = await fetch(validator+`/api/deploy`, req).then(r => r.json()).then(x =>  x)
      console.log(b)
    }
    
    const bytesToHex = (bytes) => {
      const hex = []
      for (let i = 0; i < bytes.length; i++) {
        hex.push((bytes[i] >>> 4).toString(16));
        hex.push((bytes[i] & 0xf).toString(16));
      }
      return hex.join('')
    }
    
    

    完整代码

    最后,app.js完整的代码:

    import React, {useState} from 'react'
    import Button from '@material-ui/core/Button'
    
    const { signDeploy } = require('@tgrospic/rnode-grpc-js')
    require('./rnode-grpc-gen/js/DeployServiceV1_pb')
    require('./rnode-grpc-gen/js/ProposeServiceV1_pb')
    
    const readonly = `https://observer-asia.services.mainnet.rchain.coop`
    const validator = `https://node5.root-shard.mainnet.rchain.coop`
    
    const checkBalance = async (revAddress) => {
      const req = {
        method: 'POST',
        body: `
                new return, rl(\`rho:registry:lookup\`), RevVaultCh, vaultCh in {
                  rl!(\`rho:rchain:revVault\`, *RevVaultCh) |
                  for (@(_, RevVault) <- RevVaultCh) {
                    @RevVault!("findOrCreate", "${revAddress}", *vaultCh) |
                    for (@maybeVault <- vaultCh) {
                      match maybeVault {
                        (true, vault) => @vault!("balance", *return)
                        (false, err)  => return!(err)
                      }
                    }
                  }
                }
              `
      }
    
      const  balance= await fetch(readonly+`/api/explore-deploy`, req).then(r => r.json()).then(x =>  x.expr[0].ExprInt.data)
      console.log(balance)
    
    }
    
    const bytesToHex = (bytes) => {
      const hex = []
      for (let i = 0; i < bytes.length; i++) {
        hex.push((bytes[i] >>> 4).toString(16));
        hex.push((bytes[i] & 0xf).toString(16));
      }
      return hex.join('')
    }
    
    const  transfer = async (revAddress, toAddress, amount) => { 
      const privateKey = '028670D925AF9904B790CA43B539C03521A45EF7AA9920DFAA948B85180B377E'
      const code =`
        new rl(\`rho:registry:lookup\`), RevVaultCh, vaultCh, toVaultCh, deployerId(\`rho:rchain:deployerId\`), revVaultKeyCh, resultCh in {
          rl!(\`rho:rchain:revVault\`, *RevVaultCh) |
          for (@(_, RevVault) <- RevVaultCh) {
            @RevVault!("findOrCreate", "${revAddress}", *vaultCh) |
            @RevVault!("findOrCreate", "${toAddress}", *toVaultCh) |
            @RevVault!("deployerAuthKey", *deployerId, *revVaultKeyCh) |
            for (@(true, vault) <- vaultCh; key <- revVaultKeyCh; @(true, toVault) <- toVaultCh) {
              @vault!("transfer", "${toAddress}", "${amount}", *key, *resultCh) |
              for (_ <- resultCh) { Nil }
            }
          }
        }
      `
    
      const lastBlock = await fetch(readonly + `/api/last-finalized-block`, { method:'get' })
          .then((str) => str.json()).then( x =>  x.blockInfo.blockNumber )
    
      const deployData = {
        term: code, phlolimit: 300000, phloprice: 1,
        validafterblocknumber: lastBlock
      }
      const deploy = await signDeploy(privateKey, deployData)
      const da = {
        data: {
          term: deploy.term,
          timestamp: deploy.timestamp,
          phloPrice: deploy.phloprice,
          phloLimit: deploy.phlolimit,
          validAfterBlockNumber: deploy.validafterblocknumber
        },
        sigAlgorithm: deploy.sigalgorithm,
        signature: bytesToHex(deploy.sig),
        deployer: bytesToHex(deploy.deployer)
      }
      const req = {
        method: 'POST',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(da)
      }
      const b = await fetch(validator+`/api/deploy`, req).then(r => r.json()).then(x =>  x)
      console.log(b)
    }
    
    
    function App() {
    
      const [state, setState] = useState({
        revAddress: "",
        toAddress: "",
        amount: 0
      })
    
      function handleInputChange(evt) {
        const value = evt.target.value
        setState({
          ...state,
          [evt.target.name]: value
        })
      }
    
      return (
        <div >
          <form>
            <input type="text" name="revAddress" value={state.revAddress} onChange={handleInputChange} />
            <br/><br/>
            <Button  variant="contained" color="primary" onClick={()=> checkBalance(state.revAddress)} >
              Check
            </Button>
            <br/><br/>
          </form>
          <form>
            <input type="text" name="toAddress" value={state.toAddress} onChange={handleInputChange} />
            <br/><br/>
            <input type="number" name="amount" value={state.amount} onChange={handleInputChange} />
            <br/><br/>
            <Button  variant="contained" color="primary" onClick={()=> transfer(state.revAddress, state.toAddress, state.amount)} >
              Transfer
            </Button>
          </form>
        </div>
      )
    }
    
    export default App