【Blockchain】トレーサビリティの仕組みを考える

### フロントエンド
プロセスの選択肢があり、写真を撮って、Postする。緯度、経度情報が必要な場合は付与する。

<head>
    <meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Document</title>
	<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
    <style>body {margin: 50px;}</style>
</head>
<body>
<div id="app">
    <form action="/complete" method="post" enctype="multipart/form-data">
    <h2 class="subtitle is-5">1.製品番号</h2>
    <span>1111001</span>
    <input type="hidden" name="serial_num" value="1111001"/>
    <br><br>
    <h2 class="subtitle is-5">2.製造工程を選択してください<h2>
    <select name="process" id="pet-select" class="select">
        <option value="">--Please choose an option--</option>
        <option value="1">企画デザイン</option>
        <option value="2">皮の検品</option>
        <option value="3">型抜き</option>
        <option value="4">パーツ作り</option>
        <option value="5">糊付け</option>
        <option value="6">表断面加工</option>
        <option value="7">縫製</option>
        <option value="8">仕上げ</option>
    </select>
    <br><br><br>
    <h2 class="subtitle is-5">3.背面カメラ</h2>
    <input type="file" name="file" onchange="previewFile(this);" capture="environment" accept="image/*"></label><br><br>
    <img id="preview"><br><br>
    <h2 class="subtitle is-5">4.現在地</h2>
    <span v-if="err">
        <!-- 緯度経度の情報を取得できませんでした。 -->
    </span>
    <lat></lat><br>
    <long></long><br>
    <input type="hidden" name="lat" v-model="lat"/>
    <input type="hidden" name="long" v-model="long"/>
    <br>
    <button class="button" type="submit">登録する</button>
    </form>
</div>

<script>
let lat = "35.6895014";
let long = "139.6917337";
function success(pos) {
  const crd = pos.coords;

  lat = crd.latitude;
  long = crd.longitude;
 
  let url = "/index.html?lat=" +lat + "&long=" + long;
  var app = new Vue({
    el: '#app',
    data: {
        lat: lat,
        long: long,
        err: null
    }
  })
}

function error(err) {
    Vue.component('lat',{
        template : '<span>緯度:' + lat + '</span>'
    })
    Vue.component('long',{
        template : '<span>経度:' + long + '</span>'
    })
    console.warn(`ERROR(${err.code}): ${err.message}`);
    let url = "/index.html?lat=" +lat + "&long=" + long;
    var app = new Vue({
    el: '#app',
    data: {
        lat: lat,
        long: long,
        err: err
    }
  })
  
}
navigator.geolocation.getCurrentPosition(success, error);
</script>
<script>
function previewFile(file) {
    if (file.files[0].size < 3000000) {
    var fileData = new FileReader();
    fileData.onload = (function() {
        document.getElementById('preview').setAttribute("style","width:150px;height:150px");
        document.getElementById('preview').src = fileData.result;
        
    });
    fileData.readAsDataURL(file.files[0]);
    }
}
</script>
</body>

### サーバサイド
追跡情報をチェーン上に入れるは、トランザクションとして署名して、walletから誰かのwallet宛に送らなければならない。
=> transaction detaの中に、商品識別子(serial number等)は必ず含める必要がある。
=> 商品識別子、製造プロセス、緯度、経度などの情報を全てトランザクションデータの中に入れる場合は、データ容量がある程度大きくなくてはいけないが、 商品識別子だけ入れて、後の項目はアプリケーション側(smart contract)のDBに保存すれば、データ容量は少なくて済む。その場合、アプリケーション側の開発が必要。
=> トレーサビリティに特化したブロックチェーンを作ることもできなくはないが、開発効率を考えるとナンセンス。
=> transaction dataにmessageの項目を作るのが一番良さそうには見える。

DEXとは何か? Uniswap/Raydiumを触ってみる

DEXはDecentralized Exchangesの略称で、仲介者となる企業が存在しない、ユーザ同士が直接仮想通貨を取引できるブロックチェーン上に構築される取引所

### 代表的なDEX
– Uniswap (ECR-20規格であれば全て取引できる)
– PancakeSwap
– dYdX (仮想通貨の先物取引)

ECR20は、イーサリアムのブロックチェーン上で動作するトークンの統一ルール、規格の一つ

### 日本の仮想通貨法
仮想通貨法には第一号仮想通貨、第二号仮想通貨の分類があり、前者は物品の購入などに際して不特定の者に使用できかつ不特定の者を相手として交換でき、後者は不特定の者を相手として第一号仮想通貨と交換できる
ECR20トークンはイーサリアムと交換可能なことから、第二号仮想通貨に該当する

### uniswap
metamaskなどのwalletがあれば簡単にswapができる
Uniswapでswapを実行すると、metamask上のwalletでも、swapした通貨を保有していることが確認できる。
https://app.uniswap.org/

イーサリアム: UniSwap
Binance Smart Chain: PancakeSwap
Solana : Raydium, Jupiter

Raydimでswapする場合は、PhantomなどSolのwalletを作成して、あとは接続するだけです。

RaydimもUIはUniSwapとかなり似ています。

なるほど、DEXはかなり凄いですね…

【Internet Computer】Rust ではじめてのキャニスター開発をやってみる

こちらの記事を参考にRustでDapps開発を行います。
https://smacon.dev/posts/hello-icp-rust/

### dfxのインストール
sh -ci “$(curl -fsSL https://sdk.dfinity.org/install.sh)”

dfxのインストールだが、mac m1チップのARMアーキテクチャではできない。
https://forum.dfinity.org/t/ubuntu-vm-on-macbook-m1-dfx-install-unknown-cpu-type-aarch64/21486

$ uname -m
aarch64

おいおいおい、いきなりつまづいてしまった。これのトラブルシューティングに丸一日かかりました。
どうしてもやりたかったので、結局、別の環境で作ることに。
# uname -m
x86_64

# dfxのプロジェクトファイル実行
# dfx new –type=rust rust_hello
# cd rust_hello
# dfx start –background
# rustup target add wasm32-unknown-unknown
# dfx deploy
Installed code for canister rust_hello_backend, with canister ID bkyz2-fmaaa-aaaaa-qaaaq-cai

# dfx canister call bkyz2-fmaaa-aaaaa-qaaaq-cai greet ‘(“everyone”: text)’
(“Hello, everyone!”)

# dfx canister status bkyz2-fmaaa-aaaaa-qaaaq-cai
Canister status call result for bkyz2-fmaaa-aaaaa-qaaaq-cai.
Status: Running
Controllers: bnz7o-iuaaa-aaaaa-qaaaa-cai wbqzn-2jy52-icft2-erprw-b52eq-e4ixe-4yxqb-5neaf-miz3k-arx3m-gae
Memory allocation: 0 Bytes
Compute allocation: 0 %
Freezing threshold: 2_592_000 Seconds
Idle cycles burned per day: 1_257_808 Cycles
Memory Size: 1_600_075 Bytes
Balance: 3_061_352_105_201 Cycles
Reserved: 0 Cycles
Reserved cycles limit: 5_000_000_000_000 Cycles
Wasm memory limit: 3_221_225_472 Bytes
Wasm memory threshold: 0 Bytes
Module hash: 0xb60872271f3ca7c4ee2b18a54434c344e3095982685ce10f2c7217c08dfb1cf7
Number of queries: 0
Instructions spent in queries: 0
Total query request payload size: 0 Bytes
Total query response payload size: 0 Bytes
Log visibility: controllers

$ dfx stop

どうやら、「デプロイして、キャニスターを実行」という流れになるようだ。
私の理解では、このキャニスターがスマートコントラクト。
そして、FrontはデフォルトでReactが用意されている。

なるほど、

【Rust】ECDSA署名のverifyのエラーハンドリング

署名のverifyはtrue or falseで返したいが、、、

pub async fn verify_signature(signedtransaction: &kernel::SignedTransaction) -> bool {
    // 省略
    return verifying_key.verify(posted_serialized.as_bytes(), &signature).is_ok()
}

偽のpublic keyを送ってきた可能性もあるため、Result型で返却しないといけない。

pub async fn verify_signature(signedtransaction: &SignedTransaction) -> Result<bool, Box<dyn std::error::Error>>{
    // 省略
    Ok(verifying_key.verify(posted_serialized.as_bytes(), &signature).is_ok())
}

なるほど、こういうのは、UnitTestを書いてテストしないと気づかない…

【Rust】Peer情報をnode ip listに書き込む

bitcoinのIPアドレスリストは、ipアドレスとUnix timeが対になっている
(参考↓)
https://qiita.com/onokatio/items/04f23b7300dec7e287cb
その他にも、ipv4/ipv6や、ping, lastsend, lastrec, id, versionなどの情報も含まれる。

handshake後にpeers.datのファイルに書き込むようにして、ping, pongの箇所は、enumでactive/inactiveのstatusで表現することにした。

use serde::{Serialize, Deserialize};
use std::{io::Write};
use chrono::{Utc,DateTime};
use std::fs::OpenOptions;

#[derive(Serialize, Deserialize, Clone, Debug)]
enum Status {
    ACTIVE,
    INACTIVE,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
struct Peer {
    ip: String,
    unixtime: i64,
    nodetype: String,
    version: String,
    status: Status,
}

fn main(){
    let utc_datetime: DateTime<Utc> = Utc::now();
    println!("{}", utc_datetime.timestamp());

    let peer = Peer {ip: "192.168.33.10".to_string(), unixtime: utc_datetime.timestamp(), nodetype: "c".to_string(), version: "1.0.1".to_string(), status: Status::ACTIVE};
    println!("{:?}", peer);
    let serialized: Vec<u8> = serde_json::to_vec(&peer).unwrap();
    let mut file_ref = OpenOptions::new()
                        .append(true)
                        .open("./data/peers.dat")
                        .expect("Unable to open file");
    file_ref.write_all(&serialized).expect("write failed");
    file_ref.write_all(b"\n").expect("write failed");
}

Running `target/debug/sample`
1740366182
Peer { ip: “192.168.33.10”, unixtime: 1740366182, nodetype: “c”, version: “1.0.1”, status: ACTIVE }

nodeipリストをどうゆう風にするか、ファイルを分割するか、データ型などずっと悩んでいたけど、他のチェーンの設計は参考になりますね。

ちなみに取り出す際↓

fn get_peers() -> Result<Vec<Peer>, Box<dyn std::error::Error>>  {

    let mut peers: Vec<Peer> = Vec::new();
    for result in BufReader::new(File::open("./data/peers.dat")?).lines() {
        let line: Peer = serde_json::from_str(&result?).unwrap();
        peers.push(line);
    }
    Ok(peers)
}

【Rust】簡易的なBloomfilter

ブルームフィルタを更新していくロジックがいまいちわからんが、なんとなく

use std::hash::{DefaultHasher, Hash, Hasher};

#[derive(Debug, PartialEq, Clone)]
struct BloomFilter {
    filter: [i32; 10],
}

impl BloomFilter {
    fn set_v(&mut self, val: String) {
        let list: Vec<u8> = self.n_hash(val);
        for l in list {
            let i = usize::from(l);
            if self.filter[i] == 0 {
                self.filter[i] = 1
            } else {
                self.filter[i] = 2 }
        }    
    }

    fn n_hash(&self, val: String) -> Vec<u8>{
        let hashed = siphash(val);
        let list: Vec<u8> = s_digit(hashed);
        return list
    }

    fn check_v(self, val: String) -> bool {
        let list: Vec<u8> = self.n_hash(val);
        let mut c_bf = BloomFilter { filter: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]};
        for l in list {
            let i = usize::from(l);
            if c_bf.filter[i] == 0 {
                c_bf.filter[i] = 1
            } else {
                c_bf.filter[i] = 2 }
        }
        return self == c_bf
    }
}

fn siphash(s: String) -> u64 {
    let mut siphash = DefaultHasher::new();
    s.hash(&mut siphash);
    return siphash.finish()
}

fn s_digit(n: u64) -> Vec<u8> {
    n.to_string()
        .chars()
        .into_iter()
        .map(|char| char.to_digit(10).unwrap() as u8)
        .collect()
}

fn main(){
    let mut bf = BloomFilter { filter: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]};
    bf.set_v("hello world".to_string());
    println!("{:?}", bf);
    println!("{}", bf.clone().check_v("hello world!".to_string()));
    println!("{}", bf.clone().check_v("hello world".to_string()));
}

Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.33s
Running `target/debug/wallet`
BloomFilter { filter: [2, 2, 0, 1, 2, 1, 1, 2, 2, 2] }
false
true

【Python】HD walletの親鍵/子鍵とchaincodeの概要

子の秘密鍵は、親のchaincdeと[親の公開鍵+index]をhmac_sha512でハッシュ化して作成している。

### マスター秘密鍵、公開鍵

import os
import binascii
import hmac
import hashlib
import ecdsa

seed = os.urandom(32)
root_key = b"Bitcoin seed"

def hmac_sha512(data, key_message):
    hash = hmac.new(data, key_message, hashlib.sha512).digest()
    return hash

def create_pubkey(private_key):
    publickey = ecdsa.SigningKey.from_string(private_key, curve=ecdsa.SECP256k1).verifying_key.to_string()
    return publickey

master = hmac_sha512(seed, root_key)

master_secretkey = master[:32]
master_chaincode = master[32:]

master_publickey = create_pubkey(master_secretkey)
master_publickey_integer = int.from_bytes(master_publickey[32:], byteorder="big")

if master_publickey_integer %2 == 0:
    master_publickey_x = b"\x02" + master_publickey[:32]
else:
    master_publickey_x = b"\x03" + master_publickey[:32]

print(binascii.hexlify(master_secretkey))
print(binascii.hexlify(master_chaincode))
print(binascii.hexlify(master_publickey_x))

$ python3 master_key.py
b’8a6dbaaff700682778dcbae2bc8718452fe5ed80fc9026a9b564420f8d5b0d80′
b’4ce8b10cc0c0874467d8f438c412fdbf21fba51517e668dbc4bd105af6861dec’
b’03cb15210804ca8f0d45b620832be935e2f90c3830f13f04c4bd6e8b4648f27817′
(secretkey, chaincode, pubkey)

### 子秘密鍵、子公開鍵

index = 0
index_bytes = index.to_bytes(8, "big")

data = master_publickey_x + index_bytes

result_hmac512 = hmac_sha512(data, master_chaincode)
sum_integer = int.from_bytes(master_secretkey,"big") + int.from_bytes(result_hmac512[:32],"big")

p = 2 ** 256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1
child_secretkey = (sum_integer % p).to_bytes(32,"big")

child_chaincode = result_hmac512[32:]

child_publickey = create_pubkey(child_secretkey)
child_publickey_integer = int.from_bytes(child_publickey[32:], byteorder="big")

if child_publickey_integer %2 == 0:
    child_publickey_x = b"\x02" + child_publickey[:32]
else:
    child_publickey_x = b"\x03" + child_publickey[:32]

print(binascii.hexlify(child_secretkey))
print(binascii.hexlify(child_chaincode))
print(binascii.hexlify(child_publickey_x))

b’5ff011a3e9cd672aaf0dc9fd52cb3172ac2815cb270f919135e6b0f0e6e03d54′
b’15f4d148b2d7730076d5e670249649ea8f0fd8572dad3818680e347196149dda’
b’03480a1dbb4a87d867bee3d364b608e21d685af271876707b9f9d5b75c6df6fde7′

b’23faf6fad81cd93e12c003c944ba3ef215dae714c638756386b6b9404da5aac9′
b’e3563dc6891e238cd0d8ebf99e65ebfc67cecf42364de9756b89859bbd049b62′
b’02039253af3e828bfbf1e560fe0e923a144fc4496ded3b6bbfa0d568cf7177d1c3′

なるほど、一見複雑そうに見えるが、なかなか面白いね

【Python】DH-Walletのライブラリを利用してみる

$ pip3 install hdwallet

#!/usr/bin/env python3

from hdwallet import HDWallet
from hdwallet.entropies import (
    BIP39Entropy, BIP39_ENTROPY_STRENGTHS
)
from hdwallet.mnemonics import BIP39_MNEMONIC_LANGUAGES
from hdwallet.cryptocurrencies import Bitcoin as Cryptocurrency
from hdwallet.hds import BIP32HD
from hdwallet.derivations import CustomDerivation
from hdwallet.const import PUBLIC_KEY_TYPES

import json

# Initialize Bitcoin HDWallet
hdwallet: HDWallet = HDWallet(
    cryptocurrency=Cryptocurrency,
    hd=BIP32HD,
    network=Cryptocurrency.NETWORKS.MAINNET,
    language=BIP39_MNEMONIC_LANGUAGES.KOREAN,
    public_key_type=PUBLIC_KEY_TYPES.COMPRESSED,
    passphrase="talonlab"
).from_entropy(  # Get Bitcoin HDWallet from entropy
    entropy=BIP39Entropy(
        entropy=BIP39Entropy.generate(
            strength=BIP39_ENTROPY_STRENGTHS.ONE_HUNDRED_SIXTY
        )
    )
).from_derivation(  # Drive from Custom derivation
    derivation=CustomDerivation(
        path="m/0'/0/0"
    )
)

# Print all Bitcoin HDWallet information's
print(json.dumps(hdwallet.dump(exclude={"indexes"}), indent=4, ensure_ascii=False)) 

$ python3 hd_wallet.py
{
“cryptocurrency”: “Bitcoin”,
“symbol”: “BTC”,
“network”: “mainnet”,
“coin_type”: 0,
“entropy”: “f90afa34cc3653845d06c179ba917569c60e1179”,
“strength”: 160,
“mnemonic”: “호흡 분리 여론 온종일 생신 조용히 숙소 추석 시월 청춘 사계절 철도 살림 건축 팩스”,
“passphrase”: “talonlab”,
“language”: “Korean”,
“seed”: “d79ac6b15a3782eefd52ae4b09c961ddd7263f6de4a852534497139516bc04549cb62dd2063f21090b13b58c442a35610f630034ca9ad85877ee8a544f8643b4”,
“ecc”: “SLIP10-Secp256k1”,
“hd”: “BIP32”,
“semantic”: “p2pkh”,
“root_xprivate_key”: “xprv9s21ZrQH143K35Wb8sjvrSbVYqX2xGJvX64QxwC1spNkdnakC1Mtwv2CQE1trAMGA83TGEZSjWtdh1s9pFT2P6z2PKepwjvhbkeU69UsGaE”,
“root_xpublic_key”: “xpub661MyMwAqRbcFZb4EuGwDaYE6sMXMj2mtJz1mKbdS9ujWautjYg9ViLgFWsQNQ3PjEnrdPhNhS9JvuH6WLG5pgJaTBezMoy7ecEKLK3BAvH”,
“root_private_key”: “4ba3752c7a474a3be62ef060fd79c6517b7f2af01d7e573f11685e20f55a0303”,
“root_wif”: “Kykk1CNJWm7sAsve2eDCezUykFyQfX9BavJNTWakMutPYSSHZ5kP”,
“root_chain_code”: “66708f4fdae1aec89612e24205d9244f0b8a1a4ada2fc6076e0eea8a91131831”,
“root_public_key”: “033bbd876465246c571f02df1aa65527c82dfa9779833a2840ecb63167f1f507a0”,
“strict”: true,
“public_key_type”: “compressed”,
“wif_type”: “wif-compressed”,
“derivation”: {
“at”: {
“path”: “m/0’/0/0”,
“depth”: 3,
“index”: 0
},
“xprivate_key”: “xprv9yoVptUsgY29Jz8tMnbLeoWQygM3m6zYSWdYDiVdBazjqyc11ioZHdowCxYi17fN5CwDnm3ith9tMeVjv5GToJY4S7NLrbuged6Q3DJiGWy”,
“xpublic_key”: “xpub6CnrEQ1mWuaSXUDMTp8M1wT9XiBYAZiPojZ926uEjvXiimw9ZG7oqS8R4Dev3hzwcXeqNmMiN9knr88cPtduVZ1Gqn1kchT23eB5wK7N8t1”,
“private_key”: “96ceeef7b35bfd428842d2dc08719d3c5b5cff9967a7110ece12d57af017a66b”,
“wif”: “L2GryxVSFxauZCLCQJqmV85tVNoXCXyRdC5zomodDWwd33BPjJNn”,
“chain_code”: “7a95361a7ffc067c5165411d55d1400bc5b3cb5ea2ca90741e918fd2a8fb3349”,
“public_key”: “02a0e4a452f270f2647949a57cc55909182eb2f232f4b5146ccdd232a4ffed8803”,
“uncompressed”: “04a0e4a452f270f2647949a57cc55909182eb2f232f4b5146ccdd232a4ffed88030259ef4fa9649169a66b61c26881426d68b6b51ad6ea3cb7dc0b41cc85b6ba00”,
“compressed”: “02a0e4a452f270f2647949a57cc55909182eb2f232f4b5146ccdd232a4ffed8803”,
“hash”: “1bff595df8d0d9695e612eca95afcf4c882e5f32”,
“fingerprint”: “1bff595d”,
“parent_fingerprint”: “9b0194d3”,
“addresses”: {
“p2pkh”: “13Z399RbXeELGgKRKq5sM8nNim9waZGoPW”,
“p2sh”: “35nKB4UKgHSzRYX4J36YxLFMqm7YpCkMKU”,
“p2tr”: “bc1pmsdcxya38lnkmrs62e4yuad0cgr322tp9cc7sgjrmfe3e3jasaeqcvrvwl”,
“p2wpkh”: “bc1qr0l4jh0c6rvkjhnp9m9ftt70fjyzuhejdwqt46”,
“p2wpkh_in_p2sh”: “3KNNVBigqcdPAsUU1apzbRJj5dx993YoGQ”,
“p2wsh”: “bc1qfznz7ncnv4xu29zx3cdv7dzdtgc9e600ukd0kxyca7dg6k9gswqqs8xs7w”,
“p2wsh_in_p2sh”: “36MvcLHZq8jAogs7rm8beCo6rZEybZRkzk”
}
}
}

#!/usr/bin/env python3

from hdwallet.utils import generate_passphrase
print(generate_passphrase(length=32))

$ python3 hd_wallet.py
MQIjC1twsYSqRYAYzUbXZGouqirSa65t

#!/usr/bin/env python3

from hdwallet.mnemonics.algorand import AlgorandMnemonic, ALGORAND_MNEMONIC_WORDS, ALGORAND_MNEMONIC_LANGUAGES
mnemonic: str = AlgorandMnemonic.from_words(words=ALGORAND_MNEMONIC_WORDS.TWENTY_FIVE, language=ALGORAND_MNEMONIC_LANGUAGES.ENGLISH)
print(AlgorandMnemonic.from_entropy(entropy="65234f4ec655b087dd74d186126e301d73d563961890b2f718476e1a32522329", language=ALGORAND_MNEMONIC_LANGUAGES.ENGLISH))

hole develop cheese fragile gaze giggle plunge sphere express reunion oblige crack priority ocean seven mosquito wagon glow castle plunge goddess stand empower ability empower

Secure randomとは?

暗号鍵の生成など安全な乱数を生成するためのクラス。推測されにくいランダムな数値や文字列を生成するツールとして、Java や Ruby、PowerShell などで使用されている。

【Blockchain】Walletの比較

人気のWallet比較

### coincheck(pc/mobile)
取引: 総資産、入金出金、購入、売却、送金、受取、積立、大口、NFT, チャート、トレードビュー
アカウント:アカウント変更、本人確認、電話番号、取引履歴、ログイン履歴
ログアウト

### bitcoin wallet(mobile)
btc amount, transaction, send, request, exchange rate, network monitor, block, peer情報

### Trust Crypto Wallet(mobile)
send, receive, buy, earn, crypt, NFT

### bitbank(pc/mobile)
総資産、現物、信用、保有銘柄、お気に入り、ピックアップ、入金、販売所、資産、お知らせ、メニュー、データ(履歴)、登録情報、設定、API

### SafePal wallet(app/hardware)
coin, Delfi, swap, bridge, exchange, market, favorite, deposit, Network

### coinbase wallet
Crypt, NFTs, DeFi,
Buy, swap, bridge, send, receive
Asset, Transaction, Browser, Explore, Setting

### blockchain.com
buy swap sell receive
Asset, price

### 楽天ウォレット
ホーム、運用状況、ニュース、お知らせ、メニュー

大体、どのような機能が必要かはわかりました。QRコードは必要ですね。