Open2

LINE BlockChainサービスの作り方

YuheiNakasakaYuheiNakasaka

LINEのBlockchainの資料が全て日本語でかつスマートコントラクトをGUIから作れたりして手軽そうなのでまずはこれを触ってみることにした。ドキュメントで用語の解説とかもしてくれるから初心者でも雰囲気を掴めると思う。

LINE BlockChainサービスの作り方

リソース

チュートリアル手順

  • まずLinkCinemaのチュートリアルをざっとやってみる

準備

  • LINE Businessアカウントでプロバイダーを作成する
  • チャンネルを作成する
    • 審査
      • 2日くらいで通過する
    • BlockChainを選ぶ
  • チャンネルにBlockChainサービスを作成する
    • API KeyとSecretが手に入る。一度しか表示されないのでメモする。
  • サービス用のWalletを作成する
    • Secretが手に入る。一度しか表示されないのでメモする。
  • ServiceToken(=サービス内通貨)を作成する
    • ContractIdが作られる
    • BTC,ETH,XRP,NEMとかみたいなやつのサービス内版
  • ItemToken(=クーポン券とか)を作成する
    • ContractIdが作られる
    • TokenId
      • ItemTokenを発行する時に作成される一意な識別子
      • ContractId+TokenType+TokenIndexを組み合わせて作成される
  • (任意)transactionを取得してみる
    • このPython3のコードをtutorial_request.pyとして保存して使うと良い
    • pip3 install requestsはしておく
    • SERVICE_API_KEYとかSERVICE_API_SECRETは補完してくれ
実際のコード
# walletのトランザクション一覧取得
# SERVER_URL=https://test-api.blockchain.line.me SERVICE_API_KEY= SERVICE_API_SECRET=  WALLET_ADDRESS= python3 tutorial_request.py
 
# 特定のトランザクション取得
# SERVER_URL=https://test-api.blockchain.line.me SERVICE_API_KEY= SERVICE_API_SECRET= TRANSACTION_HASH= python3 tutorial_request.py
 
# NFTの鋳造
# SERVER_URL=https://test-api.blockchain.line.me SERVICE_API_KEY= SERVICE_API_SECRET= OWNER_ADDRESS=  OWNER_SECRET= ITEM_CONTRACT_ID= python3 tutorial_request.py
 
# NFTをユーザーウォレットへ送信
# SERVER_URL=https://test-api.blockchain.line.me SERVICE_API_KEY= SERVICE_API_SECRET= OWNER_ADDRESS=  OWNER_SECRET= ITEM_CONTRACT_ID= RECEPIENT_ADDRESS= python3 tutorial_request.py

import hmac
import hashlib
import base64
import os
import requests
import random
import string
import time
import json


class RequestBodyFlattener:

    def __flatten_key_value(self, key, value):
        if (isinstance(value, str)):
            return f"{key}={value}"

        if (isinstance(value, list)):
            l_key_value = {}
            for index, ele in enumerate(value):
                for lkey in list(ele.keys() | l_key_value.keys()):
                    if lkey in ele.keys():
                        lvalue = ele[lkey]
                    else:
                        lvalue = ""

                    if (lkey in l_key_value.keys()):
                        l_key_value[lkey] = f"{l_key_value[lkey]},{lvalue}"
                    else:
                        l_key_value[lkey] = f"{',' * index}{lvalue}"
            return "&".join("%s=%s" % (f"{key}.{lkey}", lvalue) for (lkey, lvalue) in sorted(l_key_value.items()))

    def flatten(self, body: dict = {}):
        sorted_body = sorted(body.items())
        return "&".join(self.__flatten_key_value(key, value) for (key, value) in sorted_body)


class SignatureGenerator:

    def __createSignTarget(self, method, path, timestamp, nonce, parameters: dict = {}):
        signTarget = f'{nonce}{str(timestamp)}{method}{path}'
        if(len(parameters) > 0):
            signTarget = signTarget + "?"

        return signTarget

    def generate(self, secret: str, method: str, path: str, timestamp: int, nonce: str, query_params: dict = {}, body: dict = {}):
        body_flattener = RequestBodyFlattener()
        all_parameters = {}
        all_parameters.update(query_params)
        all_parameters.update(body)

        signTarget = self.__createSignTarget(method.upper(), path, timestamp, nonce, all_parameters)

        if (len(query_params) > 0):
            signTarget += '&'.join('%s=%s' % (key, value) for (key, value) in query_params.items())

        if (len(body) > 0):
            if (len(query_params) > 0):
                signTarget += "&" + body_flattener.flatten(body)
            else:
                signTarget += body_flattener.flatten(body)

        raw_hmac = hmac.new(bytes(secret, 'utf-8'), bytes(signTarget, 'utf-8'), hashlib.sha512)
        result = base64.b64encode(raw_hmac.digest()).decode('utf-8')

        return result


class LinkMovie:
    # walletのtransactionを10件取得する
    def GET_v1_wallets_walletAddress_transactions(self):
        server_url = os.environ['SERVER_URL']
        service_api_key = os.environ['SERVICE_API_KEY']
        service_api_secret = os.environ['SERVICE_API_SECRET']
        wallet_address = os.environ['WALLET_ADDRESS']

        nonce = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(8))
        timestamp = int(round(time.time() * 1000))

        path = '/v1/wallets/' + str(wallet_address) + '/transactions'

        query_params = {
            'limit': 10,
            'orderBy': 'desc',
            'page': 1
        }

        headers = {
            'service-api-key': service_api_key,
            'nonce': nonce,
            'timestamp': str(timestamp)
        }

        signatureGenerator = SignatureGenerator()
        signature = signatureGenerator.generate(service_api_secret, 'GET', path, timestamp, nonce, query_params)
        headers['signature'] = signature

        res = requests.get(server_url + path, params=query_params, headers=headers)
        data = res.json()
        return json.dumps(data, indent=2)

    # 特定のtransactionを取得する
    def GET_v1_wallets_specific_transactions(self):
        server_url = os.environ['SERVER_URL']
        service_api_key = os.environ['SERVICE_API_KEY']
        service_api_secret = os.environ['SERVICE_API_SECRET']
        transaction_hash = os.environ['TRANSACTION_HASH']

        nonce = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(8))
        timestamp = int(round(time.time() * 1000))

        path = '/v1/transactions/' + str(transaction_hash)

        query_params = {}

        headers = {
            'service-api-key': service_api_key,
            'nonce': nonce,
            'timestamp': str(timestamp)
        }

        signatureGenerator = SignatureGenerator()
        signature = signatureGenerator.generate(service_api_secret, 'GET', path, timestamp, nonce, query_params)
        headers['signature'] = signature

        res = requests.get(server_url + path, params=query_params, headers=headers)
        data = res.json()
        return json.dumps(data, indent=2)

    # 鋳造する
    def POST_v1_item_tokens_contractId_non_fungibles_tokenType_mint(self):
        owner_address = os.environ['OWNER_ADDRESS']
        owner_secret = os.environ['OWNER_SECRET']
        item_contract_id = os.environ['ITEM_CONTRACT_ID']
        server_url = os.environ['SERVER_URL']
        server_url = os.environ['SERVER_URL']
        service_api_key = os.environ['SERVICE_API_KEY']
        service_api_secret = os.environ['SERVICE_API_SECRET']

        nonce = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(8))
        timestamp = int(round(time.time() * 1000))

        path = '/v1/item-tokens/' + str(item_contract_id) + '/non-fungibles/10000001/mint'

        # 自分自身のwalletに送る
        request_body = {
            'ownerAddress': owner_address,
            'ownerSecret': owner_secret,
            'toAddress': owner_address,
            'name': 'MovieTicket',
            'meta': str(round(time.time() * 10000000))
        }

        headers = {
            'service-api-key': service_api_key,
            'nonce': nonce,
            'timestamp': str(timestamp),
            'Content-Type': 'application/json'
        }

        signatureGenerator = SignatureGenerator()
        signature = signatureGenerator.generate(service_api_secret, 'POST', path, timestamp, nonce, body=request_body)
        headers['signature'] = signature

        res = requests.post(server_url + path, headers=headers, json=request_body)
        data = res.json()
        return json.dumps(data, indent=2)

    # OwnerWalletからUserWalletにアイテムトークンを転送
    def POST_v1_wallets_walletAddress_item_tokens_contractId_non_fungibles_tokenType_tokenIndex_transfer(self):
        token_id = '00000001'
        owner_address = os.environ['OWNER_ADDRESS']
        owner_secret = os.environ['OWNER_SECRET']
        item_contract_id = os.environ['ITEM_CONTRACT_ID']
        recepient_address = os.environ['RECEPIENT_ADDRESS']
        server_url = os.environ['SERVER_URL']
        service_api_key = os.environ['SERVICE_API_KEY']
        service_api_secret = os.environ['SERVICE_API_SECRET']

        nonce = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(8))
        timestamp = int(round(time.time() * 1000))

        path = '/v1/wallets/' + str(owner_address) + '/item-tokens/' + str(item_contract_id) + \
            '/non-fungibles/10000001/' + str(token_id) + '/transfer'

        request_body = {
            'walletSecret': owner_secret,
            'toAddress': recepient_address
        }

        headers = {
            'service-api-key': service_api_key,
            'nonce': nonce,
            'timestamp': str(timestamp),
            'Content-Type': 'application/json'
        }

        signatureGenerator = SignatureGenerator()
        signature = signatureGenerator.generate(service_api_secret, 'POST', path, timestamp, nonce, body=request_body)
        headers['signature'] = signature

        res = requests.post(server_url + path, headers=headers, json=request_body)
        data = res.json()
        return json.dumps(data, indent=2)


linkMovie = LinkMovie()
# 雑だけど
if os.environ.get('RECEPIENT_ADDRESS') != None:
    print(linkMovie.POST_v1_wallets_walletAddress_item_tokens_contractId_non_fungibles_tokenType_tokenIndex_transfer())
elif os.environ.get('OWNER_ADDRESS') != None:
    print(linkMovie.POST_v1_item_tokens_contractId_non_fungibles_tokenType_mint())
elif os.environ.get('TRANSACTION_HASH') != None:
    print(linkMovie.GET_v1_wallets_specific_transactions())
else:
    print(linkMovie.GET_v1_wallets_walletAddress_transactions())

NFTの作成

Test Coinの作成

  • NFTの購入などにはCachewというTestNetのTest Coinを使う
  • GUIからのみ作成できる

LINE BITMAX Walletに登録する

  • LINE BITMAX WalletはLINE IDを基盤としたサービスユーザーのためのブロックチェーンウォレット
  • Test CoinやアイテムトークンなどLINE BlockChainのすべてのトークンを扱う
  • テストユーザーに100人まで登録できる。
  • テストユーザーに発行したTest Coinを送信可能

アイテムトークンの購入

  • 送金
    • ユーザーWallet -> サービスWallet
  • アイテムの転送
    • サービスWallet -> ユーザーWallet

サービストークンの付与

  • アイテムトークンの転送同様、API or GUI(assets -> details -> Sendページ)から特定のアドレスを指定して送信する

FT

  • 上記NFTの場合と基本的に操作でできる。性質や利用シーンが異なるだけ。

Composableトークン

  • 親子関係を結んだトークンをいいます。親と子は1対多で紐付き、このように紐付けられたトークンはまるで1つのトークンのように取り扱うことができます。代表例が、キャラクターに装備を装着することです。キャラクターといろんな装備がそれぞれのトークンであれば、キャラクターは親、各装備は子になります。 Composableトークンで親子関係になるのようにすることを結合(attach)といい、関係を断つことを分離(detach)といいます。

  • アイテムトークンを2つ(A,B)用意する。親になるアイテムトークンAに、子になるアイテムトークンBをAPIで結合する。
  • 転送は親のアイテムトークンを送るだけで良い。