Symbol投げたら家の鍵が開いた話

# はじめに
[nem Advent Calendar 2021](https://qiita.com/advent-calendar/2021/nem) 22日目の記事です。

**身近な生活動線上にSymbolを置いてあげるともっと色んな層に知ってもらえるのでは？**

ということで、普段の生活で何気なく行う**玄関鍵の開け閉め**をSymbolでやってみました。
本記事で触れている内容は以下の3つです。

- 着金を許可するアカウントを制限する
-  Symbolトランザクションを発行する
-  着金を検知した後、鍵の開閉を行う

※話が複雑になるので今回はアプリ側の実装には触れません。

# 概要
指定したアドレスからのSymbolトランザクションを検知し、スマートロック製品 [SESAME（セサミ）](https://jp.candyhouse.co/)を操作します。
SESAMEはAPIを利用してステータスの確認や鍵の開閉を行うことができます。便利。

動作イメージはこちら↓
何度か試したところ、**ボタンクリックから鍵が開くまで平均約25秒** かかっていました。
※検証はTestnetで行っています。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Symbolスマートロックできました🔐<br>トランザクションが承認されると鍵が動きます。手数料「早い」で設定してこの応答速度でした(スマートロック純正アプリはほぼタイムラグ無いのでその点は時代が追いつくのを待ってください)<br><br>イラスト提供はたろう(<a href="https://twitter.com/watashi_taro_0?ref_src=twsrc%5Etfw">@watashi_taro_0</a> )さん😍<br>かわいい！<a href="https://twitter.com/hashtag/Symbol?src=hash&amp;ref_src=twsrc%5Etfw">#Symbol</a> <a href="https://twitter.com/hashtag/XYM?src=hash&amp;ref_src=twsrc%5Etfw">#XYM</a> <a href="https://t.co/ZnYbxb9pRC">https://t.co/ZnYbxb9pRC</a> <a href="https://t.co/wcX1T2fmVp">pic.twitter.com/wcX1T2fmVp</a></p>&mdash; りょー/SymbolWallet「EXYM」開発エンジニア𓊝 (@ry0_cha_ng) <a href="https://twitter.com/ry0_cha_ng/status/1472506657452490753?ref_src=twsrc%5Etfw">December 19, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

![structure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239577/aba77c10-6aa6-5c67-a8ac-69e0824d946c.png)



# 着金を許可するアカウントを制限する
誰にでも開けられる鍵では意味がないので**指定したアドレスから送られてくるトランザクションの受信だけを許可する**よう設定します。
今回は使いませんが、指定アドレスから送られてくるトランザクションを拒否することも可能です。

https://docs.symbolplatform.com/ja/concepts/account-restriction.html

1. Symbolデスクトップウォレットを開き、左ペイン`アカウント`からアカウント制限を選択します。
![アドレス制限1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239577/73db3d0b-7227-772a-eaae-4ca543428312.png)

2.　左ペイン`アドレス制限`から以下の通り設定します。

- 方向：`受信`
- タイプ：`許可`
- 宛先：`受信を許可するアドレス`（トランザクションを発行するアカウント）
- 手数料：`早い`（なるたけ早く開閉操作を行うため）
![アドレス制限3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239577/660553c8-3560-1835-d811-aa4d7d157a08.png)

![アドレス制限5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239577/33ac6296-628b-2e9a-5d56-2daed10e09e0.png)


受信を許可していないアドレスからトランザクションを発行してみると、想定通り弾かれていることが確認できます。
![アドレス制限7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239577/4e8f59f9-07d9-e567-5de6-06ab6e245bc5.png)

# Symbolトランザクションを発行する
鍵を操作するためのトランザクションを発行します。
公式ドキュメントを参考にしました。

https://docs.symbolplatform.com/ja/getting-started/first-application.html

nodeUrlは[こちら](https://symbolnodes.org/nodes_testnet/)から。今回の検証では「symbol-test.next-web-technology.com」ノードをお借りしました。

```Typescript:
import {
  Account,
  Address,
  Deadline,
  Mosaic,
  MosaicId,
  PlainMessage,
  RepositoryFactoryHttp,
  TransferTransaction,
  UInt64,
} from "symbol-sdk";

const nodeUrl = 'https://symbol-test.next-web-technology.com:3001';
const repositoryFactory = new RepositoryFactoryHttp(nodeUrl);
const transactionHttp = repositoryFactory.createTransactionRepository();
const mosaicIdHex = '3A8416DB2D53B6C8'; // Testnetのsymbol.xym
const mosaicId = new MosaicId(mosaicIdHex);
const rawAddress = 'TAD4H5-3YBHPJ-7XKBD6-RVXSFL-WBVNOA-PVF7PH-7IY'; // 受取側アカウントのSymbolアドレス
const recipientAddress = Address.createFromRawAddress(rawAddress);

const sendTx = async () => {
  const networkType = await repositoryFactory.getNetworkType().toPromise();
  const epochAdjustment = await repositoryFactory.getEpochAdjustment().toPromise();
  const deadline = Deadline.create(epochAdjustment);  // トランザクションの有効期限
  const networkGenerationHash = await repositoryFactory.getGenerationHash().toPromise();
  const signerPrivateKey = 'xxxxxxxxxxxxxxxxxxxxxx';  // 送信側アカウントの秘密鍵
  const signerAccount = Account.createFromPrivateKey(signerPrivateKey, networkType);
  
  /* トランザクションを作成する */
  const transferTransaction = TransferTransaction.create(
    deadline,
    recipientAddress,
    [new Mosaic(mosaicId, UInt64.fromUint(1000000))],    // 送信するモザイクと金額（今回は1XYM）
    PlainMessage.create('Hello, SESAME'),   // トランザクションに添えるメッセージ
    networkType,    // Mainnet or Testnet
    UInt64.fromUint(2000000),    // 手数料率
  );

  /* トランザクションに署名する */
  const signedTransaction = signerAccount.sign(transferTransaction, networkGenerationHash);

  /* 署名したトランザクションをネットワークにアナウンスする */
  return await transactionHttp.announce(signedTransaction).toPromise();
}

sendTx();
```

# 着金を検知した後、鍵の開閉を行う

着金検知にはSymbol-SDKの [Listener](https://docs.symbolplatform.com/symbol-sdk-typescript-javascript/0.20.2/interfaces/_infrastructure_ilistener_.ilistener.html) を利用します。
大まかな流れは以下の通りです。

- リスナーを起動する
- 着金を検知する
- 鍵が開いているか/閉まっているかを確認する
- 開閉を実行する
- 実行結果を返す

API Keyの取得方法とAPIリファレンスはこちらです。

https://jp.candyhouse.co/blogs/how-to/api%E3%82%AD%E3%83%BC%E5%8F%96%E5%BE%97%E6%96%B9%E6%B3%95%E3%81%A8%E3%82%BB%E3%82%B5%E3%83%9Fid%E3%81%AE%E7%A2%BA%E8%AA%8D%E6%96%B9%E6%B3%95

https://docs.candyhouse.co/#sesame-api


```Typescript:
import axios from 'axios';
import { RepositoryFactoryHttp } from "symbol-sdk";

const nodeUrl = 'https://symbol-test.next-web-technology.com:3001';
const repositoryFactory = new RepositoryFactoryHttp(nodeUrl);
const listener = repositoryFactory.createListener();

/* 受取側アドレスにトランザクションが届いたことを確認して鍵の開閉を行う */
const openListener = () => {
  /* リスナーを起動する */
  listener.open().then(() => {
    /* 送信側からトランザクションを投げる */
    // sendTx();
    /* 最新ブロックを購読する */
    listener.newBlock().subscribe(
      () => toggleDevice(),
      (err) => console.error(err),
    );
  });
}

const toggleDevice = () => {
  /* SESAMEダッシュボードから取得したAPI Key */
  api_key = 'your api key'
  device_id = 'your device id'
  const header = {"Authorization": apiKey}
  
  /* エンドポイントURL */
  const controlUrl = 'https://api.candyhouse.co/public/sesame/' + deviceId;
  const resultUrl = 'https://api.candyhouse.co/public/action-result?task_id=';
  
  /* SESAMEのステータスを確認する */
  axios.get(controlUrl, {headers: header})
  .then(resp => {
    console.log(resp.data); // { locked: true, battery: 100, responsive: true }

    const lockStatus = resp.data['locked'];
    let command;
    if (lockStatus) {
      /* 鍵がかかっているとき */
      command = 'unlock';
    } else if (!lockStatus) {
      /* 鍵が閉まっているとき */
      command = 'lock';
    } else {
      console.log('error');
    }
    const data = {"command": command};

    /* 鍵の開閉を行う */
    return axios.post(controlUrl, data, {headers: header});

  }).then(result => {
    console.log(result.data); // { task_id: 'xxxxxxxxxxxxxxx' }
    return axios.get(resultUrl + result.data['task_id'], {headers: header});

  }).then(async (processingStatus) => {
    console.log(processingStatus.data); // {'task_id': 'xxxxxxxxxx', 'status': 'processing(or terminated)'}

    // statusが完了(terminated)になるまで待つ
    let taskId;
    const _sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
    let status = processingStatus.data['status'];
    while (status != 'terminated') {
      await _sleep(2000);
      taskId = processingStatus.data['task_id'];
      let process = await axios.get(resultUrl + taskId, {headers: header});
      if (process.data['status'] === 'terminated') {
        break;
      } else {
        status = process.data;
      }
    }
    return axios.get(controlUrl, {headers: header});

  }).then(resp => {
    let replyMessage;
    const lockStatus = resp.data['locked'];
    if (lockStatus) {
      replyMessage = '鍵は閉まっています';
    } else if (!lockStatus) {
      replyMessage = '鍵が開いています';
    } else {
      console.log('error');
    }

    /* リスナーを停止する */
    listener.close();

    return replyMessage;

  }).catch(err => {
    console.log(err)
    return err;
  })
}

openListener();
```

# おわりに

「単に日常使いするならSESAME純正アプリでええやろ」となりますが、こういった類の仕組みはユースケースによって重きを置くところが変わってくるものとも思います。「開閉履歴を改ざんできない点はどこかで役立つはず」との声もちらほら挙がっていたように、実社会での活用事例は後からついてくるものと信じています。

『Symbolによる新しいモノづくり』と並行して『既存の仕組みをSymbolで再現してみる』系の事例が増えてくると認知も広がってもっと面白くなりそうな予感。アイデア思いついたらバシバシ書き出してみましょう。

生活動線上にSymbolがある日常、どうですか。