JKになりたい

何か書きたいことを書きます。主にWeb方面の技術系記事が多いかも。

uPort活用するためにとりあえずデモアプリを紐解く

uPortとは

uPortは個人に紐づく情報をブロックチェーン上に保存しておき、こちらが承認した相手に対してのみ一部を公開するといった仕組みを実現するものになります。

例えば、今までなら住所などの個人情報をAmazon楽天、その他ECサイト全てにおいて提供する必要がありましたが、uPortを介す事によって他社は個人情報を管理する必要がなくなります。

つまり、今までの「ECサイトに個人情報を登録しECサイト側が個人情報を管理」から、「ブロックチェーン上に個人情報を登録し、ECサイト側が必要な情報にアクセスする許可をユーザが出す」といった形に変わります。

既にスイスの一部ではこのuPortを利用した認証が活用されはじめているようですね。

consensysmediajapan.com

具体的な仕組みはどうなっているの、とかブロックチェーン上に個人情報を記録して大丈夫なの、とかそういった事は以下の記事が良くまとまっていて良さそうです。

zoom-blc.com

本記事では、仕組みではなく実際に実装していくにはどうしたら良いかをデモアプリから紐解いて行こうと思います。

以降は個人のスマートフォンにuPortのアプリがインストールされていることを前提とします。

デモアプリの起動→接続

uport-projectではuPortの動作を体験するデモアプリが公開されています。こちらを利用します。

github.com

https://demo.uport.meにアクセスすればすぐに体験できるのですが、これではコードをいじる事ができないのでローカルで起動しましょう。

$ git clone git@github.com:uport-project/demo.git
$ yarn install
$ npm run start

(10系のnodeを使っているとyarn installでこけたので、6.5まで下げました)

上記のコマンドを実行すると、localhost:3000でアプリが立ち上がったと思います。

アクセスすると以下のような画面が表示されます。

f:id:sakata_harumi:20180814144540p:plain

「Connect With uPort」をクリックするとQRコードが出るのでそれをスマホアプリのuPortで読み取ります。

以下のモーダルが出現し、どのような個人情報をリクエストしているのかがわかります。Continueを押して許可しましょう。

f:id:sakata_harumi:20180814144815p:plain

ここまでのコードを追っていきたいと思います。

まず、uPortの初期化はuportSetup.jsに記載されています。

import { Connect, SimpleSigner } from 'uport-connect'

const uport = new Connect('uPort Demo', {
  clientId: 'xxxxxxxxxxxxxxxxx',
  signer: SimpleSigner('xxxxxxxxxxxxx')
})

const web3 = uport.getWeb3()
export { web3, uport }

Connectオブジェクトを生成するときにどのネットワークへアクセスするかを決定しています。

clientIdとsignerはuPortのUport App Managerから生成します。デフォルトネットワークはrinkebyのようです。

次は、最初のViewであるcomponents/Welcome.jsです。

こちらはボタンを押すと以下の関数が呼ばれます。

connectUport () {
    uport.requestCredentials(
      { requested: ['name', 'phone', 'country', 'avatar'],
        notifications: true }
    ).then((credentials) => {
        console.log({credentials})
        this.props.actions.connectUport(credentials)
    })
  }

どうやら、uport.requestCredentialsをcallするとQRコードのモーダルが表示されるようです。 (この見た目を変えるために自分たちのURIをインジェクションすることもできます)

requestedで要求する情報を指定しているのがわかります。

この場合ですと、credentialsは以下のようなobjectが入ってきます。

{
    credentials: {
        address: "*****",
        avatar: {
            uri: "*****"
        },
        country: "*****",
        did: "*****"",
        name: "*****",
        networkAddress: "*****",
        phone: "*****",
        publicEncKey: "*****",
        publicKey: "*****",
        publishToken: "*****"
    }
}

要求した要素であるname,phone,country,avatarが返ってきているのがわかります。

ちなみに、存在しない要素をリクエストした場合はその要素は返ってきません。特にエラーになることはないのでクライアント側でうまくバリデーションが必要そうです。

トランザクションの発行

接続が完了すると、以下の画面に遷移します。

f:id:sakata_harumi:20180814153432p:plain

ここではスマートコントラクトに対してトランザクションを発行する事ができます。

トランザクションの送信に成功すると、Your current sharesがto Buyした数だけ増えることが確認できます。

Shares to Buyに適当な数値を入力して、「Buy Shares」を押してみましょう。

すると、アプリ側にトランザクションの送信許可を求めるモーダルが表示されます。

f:id:sakata_harumi:20180814153759p:plain

許可をしトランザクションが送信されブロックに取り込まれると、Your current sharesの数値が増えていると思います。

では、このあたりのコードを見ていきましょう。compenents/SignTransaction.jsです。

現在のshares数の取得

まず、現在のshares数を取得する部分を見ていきます。

getCurrentShares () {
  // TODO: Dump this check once MNID is default behavior
  const addr = checkAddressMNID(this.props.uport.networkAddress)
  const actions = this.props.actions
  getShares(addr, actions)
}

checkAddressMNIDですが、以下のようになっています。

const mnid = require('mnid')

function checkAddressMNID (addr) {
  if (mnid.isMNID(addr)) {
    return mnid.decode(addr).address
  } else {
    return addr
  }
}

まず、これは何でしょうか・・。

mnidはuport-projectの1つでリポジトリは↓になります。

github.com

どうやらmnidは「Multi Network Identifier」の略で、ネットワークが違う事が原因のアドレスの誤りを防ぐために存在しているようです。

例えば、ropstenテストネットワークに0xaaというアドレスがあったとして、そこに送金すると仮定します。

この時、誤ってmain-netの0xaaに向かって送信してしまうと、ETHが消失してしまいます。

これを防ぐ仕組みがmnidです。やっていることはnetworkIdとaddressをまとめてエンコーディングするだけです。

mnid.encode({
  network: '0x1',
  address: '0x00521965e7bd230323c423d96c657db5b79d099f'
})

//return: '2nQtiQG6Cgm1GYTBaaKAgr76uY7iSexUkqX'

mnidをデコードするとnetworkIdとaddressが手に入りますので、networkIdがコントラクトを実行しているネットワークと一致するかどうかを検証すれば間違った送信を防ぐことができます。

ただこのアプリでは、単に入力された値がmnidだったらそこからアドレスを取り出しているだけで、ネットワークIDの検証はしていないようです。

さて、本題のgetShares(addr, actions)ですがこの関数の中身は以下です(一部抜粋)。SharesContractを呼び出しているだけですね。

SharesContract.getShares.call(addr, (error, sharesNumber) => { 数値をセット});

SharesContractは以下でセットアップしています。

function SharesContractSetup () {
  let SharesABI = web3.eth.contract([{"constant":false,"inputs":[{"name":"share","type":"uint256"}],"name":"updateShares","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"addr","type":"address"}],"name":"getShares","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"}])
  let SharesContractObj = SharesABI.at('xxxxxxxxxxxxxxxxxx')
  return SharesContractObj
}

const SharesContract = SharesContractSetup()

export default SharesContract

特筆することはなさそうです。jsonInterfaceとコントラクトのアドレスを渡しているだけですね。

ただ、web3.jsの1系ではAPIが変わっているのでそこだけ注意です。

web3.eth.Contract — web3.js 1.0.0 documentation

また、値を読み取っているだけですのでトランザクションの発行は必要ありません。

トランザクションの発行(sharesのアップデート)

sharesのアップデート部分では、buyShares関数が呼ばれます。部分的に抜粋します。

SharesContract.updateShares(sharesNumber, (error, txHash) => {
  console.log('updateShares')
  if (error) { this.props.actions.buySharesERROR(error) }
  waitForMined(addr, txHash, { blockNumber: null }, actions,
    () => {
      this.props.actions.buySharesPENDING()
    },
    (total) => {
      console.log('waitForMined complete')
      this.props.actions.buySharesSUCCESS(txHash, total)
    }
  )
})

こちらもSharesContract.updateSharesをcallしているだけですね。 uPortで生成したweb3オブジェクトを使うと、普通にコントラクトにトランザクションを発行するだけで認証リクエストがアプリに届くようです!すごい!

waitForMinedはトランザクションが取り込まれるまで定期的に状態をポーリングして待機しているだけですので割愛します。

証明トークンを受け取る

先ほどの画面からNextを押すと、以下の画面に遷移します。

f:id:sakata_harumi:20180814163241p:plain

いずれかの「Get」を押すと、アプリに証明トークン(という訳でいいのかわからないが)を受け入れるかどうかの通知が届きます。

f:id:sakata_harumi:20180814164246p:plain

受け入れた証明トークンはVerificationsタブに表示されます。

また、アプリ側からは証明トークンを要求することが可能になります。

connectUport () {
  uport.requestCredentials(
    { requested: ['name', 'phone', 'country', 'avatar', 'Relationship'],
      notifications: true }
  ).then((credentials) => {
      console.log({credentials})
      this.props.actions.connectUport(credentials)
  })
}

上記のコードを実行すると、Relationshipトークンが存在する場合以下のようにレスポンスにRelationshipが含まれた状態で返ってきます。

f:id:sakata_harumi:20180814165318p:plain

これは、uport.attestCredentialsメソッドにより実現できるようです。 以下は期限が30日のRelationship証明トークンを発行する例です。

uport.attestCredentials({
  sub: this.props.uport.address,
  claim: {Relationship: "User"},
  exp: new Date().getTime() + 30 * 24 * 60 * 60 * 1000 // 30 days from now
})

以上が、このデモアプリで使われているuPortの機能になります。

特定の管理者に個人情報を管理されなくて良いというメリットももちろんですが、 スマホでも簡単にトランザクションに署名することができるので、uPortが使われるようになればスマホのDAppsも一般的なものになるかもしれないですね。