Tezos で独自トークンをデプロイする


[前回の記事](https://qiita.com/cedretaber/items/4b3b5cb48c475de8f3f4)で独自トークンのスマートコントラクトを作成した。
少し間が空いてしまったが、このスマートコントラクトをデプロイしてみよう。

勿論、本物の Tezos ネットワークではなくて ghostnet （テスト用の環境）を利用する。

## 環境の準備

Tezos の環境の準備については、前回の記事で書いた事と同じなので省略する。

## Temple Wallet の準備

さて、今までは Tezos の操作は手元の計算機の上で CLI でのみ行っていたが、今回はそれに加えて外部のウォレットツールを利用する。
ウォレットは何でも良いのだが、今回は Temple Wallet を使ってみた。

[Temple Wallet](https://templewallet.com/)

Tezos 用の暗号通貨ウォレットアプリであり、 Tezos そのものだけでなく Tezos のスマートコントラクトで実装されたトークンも扱うことができる。
そう、今回自作したトークンも、このアプリで取り扱うことができるというわけだ。
後でウォレットで生成されたアドレスを利用するので、天下り的ではあるがここでアカウントを作成しておこう。

アカウントの作り方は省略。
ブラウザのエクステンションを導入する方法で行うのが簡単だと思う。
（以下、ブラウザエクステンションを利用する方法で説明を行う。）

なお、 Temple Wallet は一つのアカウントの中でネットワークを切り替えて利用できる。
今回は当然 ghostnet を利用していくわけだが、本物の暗号通貨を取り扱うメインネットに接続することもできてしまう。
今自分が使っているネットワークが ghostnet である事を必ず確認して欲しい。

![スクリーンショット 2022-11-26 225345.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34175/7d6f8375-b5a6-5885-f895-0414378bfc49.png)

### Tezos を入手しておく

この後、 Temple Wallet からも操作を行うので、ウォレットのアカウントにも Tezos を入れておきたい。
以下の Faucet で Temple Wallet のアカウントをリンクすると、ウォレットで直接 Tezos を受け取ることができる。

https://faucet.marigold.dev/

## Michelson ファイルの準備

それでは、記述した SCaml のソースコードをコンパイルして Michelson のコードにしよう。
以前スマートコントラクトを作成した際と同じく、 `scamlc` を用いて行う。

```bash
$ ./scamlc myfa12.ml
```

これで Michelson ファイルが生成された。

### init を作る

さて、[前にスマートコントラクトをデプロイした際](https://qiita.com/cedretaber/items/cad89f0d3e09da2dd13d)は、この Michelson コードをデプロイして終わり、だったのだが今回はそういうわけにはいかない。
この独自コインのスマートコントラクトは状態を持っており、その初期値を決めなければいけないからだ。

初期値は Michelson のコードで指定するのだが、折角 SCaml でコントラクトを書いたのに、そのストレージ状態を Michelson で手書きしないといけないのは手間だ。
幸い、 SCaml のコードで初期状態を記述し、それを Michelson に手早く翻訳することができるのでその方法を使う。
まず以下のようなコードを書く。

```ml:myfa12_init.ml
open SCaml
open Myfa12

let init = {
  tokens= BigMap [Address "address_of_your_wallet_account", Nat 100];
  allowances= BigMap [];
  total_supply= Nat 100
}
```

ストレージの初期値を、 SCaml のコードで定義している。
`address_of_your_wallet_account` というアドレスは、 Temple Wallet で作成したアカウントのアドレスだ。ウォレット内で確認できるのでそれで置き換えて欲しい。

![スクリーンショット 2022-11-26 224459.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34175/b041f8b3-7ac2-410f-1d93-9979fcd6575c.png)


手元のクライアントで作成したアドレスを使っても良いのだが、後で手っ取り早く操作する為に最初からこちらを指定した。

これを、以下のようなコマンドライン引数付きでコンパイルする。

```bash
$ ./scamlc --scaml-convert myfa12_init.ml 
```

すると、下のように標準出力に Michelson コードが吐き出されるので、それをコピーして利用すれば良いというわけだ。

```bash
init: Pair { Elt "address_of_your_wallet_account" 100
           }
           (Pair { }
                 100)
Nothing to link...
```

### ghostnet にデプロイ

ストレージの初期値も作成できたので、いよいよ ghostnet にデプロイしよう。
デプロイするスマートコントラクトのコードはファイルで、初期値はコマンドライン引数で直接渡す。

```bash
$ ./tezos-client originate contract myfa12 \
    transferring 0 from myself \
    running myfa12.tz \
    --init 'Pair { Elt "address_of_your_wallet_account" 100 } (Pair { } 100)' \
    --burn-cap 100
```

少し時間がかかるが、こんな感じの反応があれば成功だ。

```bash
Warning:

                 This is NOT the Tezos Mainnet.

           Do NOT use your fundraiser keys on this network.

Node is bootstrapped.
Estimated gas: 2040.656 units (will add 100 for safety)
Estimated storage: 1875 bytes added (will add 20 for safety)
Operation successfully injected in the node.
Operation hash is 'onvHvmvdRgQQ5oSJCxUNyLPcwzXGobpL5mHK2sqmWcpLpRddQTd'
Waiting for the operation to be included...
Operation found in block: BKtWvvZKYnHhoSpGixFpT6L7zuuB25BbtpdfVLvybF1af343ZGW (pass: 3, offset: 3)
This sequence of operations was run:
  Manager signed operations:
    From: tz1f2SkK5c61MLCT6Va1VRtqCo4vx9ufXVD8
    Fee to the baker: ꜩ0.001977
    Expected counter: 12857148
    Gas limit: 2141
    Storage limit: 1895 bytes
    Balance updates:
      tz1f2SkK5c61MLCT6Va1VRtqCo4vx9ufXVD8 ... -ꜩ0.001977
      payload fees(the block proposer) ....... +ꜩ0.001977
    Origination:
      From: tz1f2SkK5c61MLCT6Va1VRtqCo4vx9ufXVD8
      Credit: ꜩ0
      Script:
        { parameter
            (or (or (pair %transfer (address %from) (pair (address %to_) (nat %value)))
                    (pair %approve (address %spender) (nat %value)))
                (or (pair %getAllowance
                       (pair %request (address %owner) (address %spender))
                       (contract %callback nat))
                    (or (pair %getBalance (address %owner) (contract %callback nat))
                        (pair %getTotalSupply (unit %request) (contract %callback nat))))) ;
          storage
            (pair (big_map %tokens address nat)
                  (pair (big_map %allowances (pair (address %owner) (address %spender)) nat)
                        (nat %total_supply))) ;
          code { UNPAIR ;
                 IF_LEFT
                   { IF_LEFT
                       { UNPAIR ;
                         SWAP ;
                         UNPAIR ;
                         DUP 4 ;
                         UNPAIR ;
                         SWAP ;
                         CAR ;
                         PUSH mutez 0 ;
                         AMOUNT ;
                         COMPARE ;
                         NEQ ;
                         IF { PUSH string "DontSendTez" ; FAILWITH } {} ;
                         DUP 5 ;
                         SENDER ;
                         COMPARE ;
                         EQ ;
                         IF {}
                            { SENDER ;
                              DUP 6 ;
                              PAIR ;
                              DUP 5 ;
                              PUSH nat 0 ;
                              DUP 4 ;
                              DUP 4 ;
                              GET ;
                              IF_NONE {} { DIP { DROP } } ;
                              SUB ;
                              ISNAT ;
                              IF_NONE { PUSH string "NotEnoughAllowance" ; FAILWITH } {} ;
                              DIG 2 ;
                              PUSH nat 0 ;
                              DUP 3 ;
                              COMPARE ;
                              EQ ;
                              IF { SWAP ; DROP ; NONE nat } { SWAP ; SOME } ;
                              DIG 2 ;
                              UPDATE } ;
                         DUP 4 ;
                         PUSH nat 0 ;
                         DUP 4 ;
                         DUP 8 ;
                         GET ;
                         IF_NONE {} { DIP { DROP } } ;
                         SUB ;
                         ISNAT ;
                         IF_NONE { PUSH string "NotEnoughBalance" ; FAILWITH } {} ;
                         DIG 2 ;
                         PUSH nat 0 ;
                         DUP 3 ;
                         COMPARE ;
                         EQ ;
                         IF { SWAP ; DROP ; NONE nat } { SWAP ; SOME } ;
                         DIG 5 ;
                         UPDATE ;
                         DIG 3 ;
                         PUSH nat 0 ;
                         DUP 3 ;
                         DUP 6 ;
                         GET ;
                         IF_NONE {} { DIP { DROP } } ;
                         ADD ;
                         DIG 4 ;
                         GET 4 ;
                         DIG 3 ;
                         PAIR ;
                         DIG 2 ;
                         PUSH nat 0 ;
                         DUP 4 ;
                         COMPARE ;
                         EQ ;
                         IF { DIG 2 ; DROP ; NONE nat } { DIG 2 ; SOME } ;
                         DIG 3 ;
                         UPDATE ;
                         PAIR ;
                         NIL operation ;
                         PAIR }
                       { UNPAIR ;
                         DUP 3 ;
                         GET 3 ;
                         PUSH mutez 0 ;
                         AMOUNT ;
                         COMPARE ;
                         NEQ ;
                         IF { PUSH string "DontSendTez" ; FAILWITH } {} ;
                         SWAP ;
                         SENDER ;
                         PAIR ;
                         PUSH nat 0 ;
                         PUSH nat 0 ;
                         DUP 4 ;
                         DUP 4 ;
                         GET ;
                         IF_NONE {} { DIP { DROP } } ;
                         COMPARE ;
                         GT ;
                         IF { PUSH nat 0 ; DUP 4 ; COMPARE ; GT } { PUSH bool False } ;
                         IF { DROP 4 ; PUSH string "UnsafeAllowanceChange" ; FAILWITH } {} ;
                         DUP 4 ;
                         GET 4 ;
                         DIG 2 ;
                         PUSH nat 0 ;
                         DUP 5 ;
                         COMPARE ;
                         EQ ;
                         IF { DIG 3 ; DROP ; NONE nat } { DIG 3 ; SOME } ;
                         DIG 3 ;
                         UPDATE ;
                         PAIR ;
                         SWAP ;
                         CAR ;
                         PAIR ;
                         NIL operation ;
                         PAIR } }
                   { IF_LEFT
                       { UNPAIR ;
                         PUSH mutez 0 ;
                         AMOUNT ;
                         COMPARE ;
                         NEQ ;
                         IF { PUSH string "DontSendTez" ; FAILWITH } {} ;
                         DUP 3 ;
                         NIL operation ;
                         DIG 3 ;
                         PUSH mutez 0 ;
                         PUSH nat 0 ;
                         DIG 6 ;
                         GET 3 ;
                         DIG 6 ;
                         GET ;
                         IF_NONE {} { DIP { DROP } } ;
                         TRANSFER_TOKENS ;
                         CONS ;
                         PAIR }
                       { IF_LEFT
                           { UNPAIR ;
                             PUSH mutez 0 ;
                             AMOUNT ;
                             COMPARE ;
                             NEQ ;
                             IF { PUSH string "DontSendTez" ; FAILWITH } {} ;
                             DUP 3 ;
                             NIL operation ;
                             DIG 3 ;
                             PUSH mutez 0 ;
                             PUSH nat 0 ;
                             DIG 6 ;
                             CAR ;
                             DIG 6 ;
                             GET ;
                             IF_NONE {} { DIP { DROP } } ;
                             TRANSFER_TOKENS ;
                             CONS ;
                             PAIR }
                           { PUSH mutez 0 ;
                             AMOUNT ;
                             COMPARE ;
                             NEQ ;
                             IF { PUSH string "DontSendTez" ; FAILWITH } {} ;
                             CDR ;
                             NIL operation ;
                             SWAP ;
                             PUSH mutez 0 ;
                             DUP 4 ;
                             GET 4 ;
                             TRANSFER_TOKENS ;
                             CONS ;
                             PAIR } } } } }
        Initial storage:
          (Pair { Elt "tz1YrCaL8MZneW6x6gKrbBEwFEnX7gX7fEMm" 100 } (Pair {} 100))
        No delegate for this contract
        This origination was successfully applied
        Originated contracts:
          KT19f9AUftbBxs5uF98tdEvFHrxpAG8NdrCd
        Storage size: 1618 bytes
        Updated big_maps:
          New map(203679) of type (big_map (pair address address) nat)
          New map(203678) of type (big_map address nat)
          Set map(203678)[0x000090e6e06940ec9fae42ff0e85bfb0709f1f012897] to 100
        Paid storage size diff: 1618 bytes
        Consumed gas: 2040.656
        Balance updates:
          tz1f2SkK5c61MLCT6Va1VRtqCo4vx9ufXVD8 ... -ꜩ0.4045
          storage fees ........................... +ꜩ0.4045
          tz1f2SkK5c61MLCT6Va1VRtqCo4vx9ufXVD8 ... -ꜩ0.06425
          storage fees ........................... +ꜩ0.06425

New contract KT19f9AUftbBxs5uF98tdEvFHrxpAG8NdrCd originated.
The operation has only been included 0 blocks ago.
We recommend to wait more.
Use command
  tezos-client wait for onvHvmvdRgQQ5oSJCxUNyLPcwzXGobpL5mHK2sqmWcpLpRddQTd to be included --confirmations 1 --branch BLcqF55BtAYcaTYMwRR6JPWtTe92S3V73en3UVn35kmQFSPNm7o
and/or an external block explorer.
Contract memorized as myfa12.
```

上手くデプロイできた。
……こんな簡単にデプロイできて、本当にちゃんと使うことができるのだろうか？

## 自作トークンを使ってみる

余りにあっけなくデプロイできたものだから、本当に使えるのか怪しんでしまうのも仕方ないだろう。
きちんと機能が実現できることを確認するために、実際に利用してみよう。

さて、この時に最初に作った Temple Wallet アカウントを利用する。

### 自作トークンを登録する

Temple Wallet には Tezos 以外のトークンを管理対象として登録することができるのだが、最初に書いたとおり、自作トークンでも扱える。
「管理」「トークンを追加」と選択し、アドレスの所にデプロイしたスマートコントラクトのアドレスを入力する。

![スクリーンショット 2022-11-26 224726.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34175/1fea7b19-1716-7f4e-131d-08c1f5172c82.png)
![スクリーンショット 2022-11-26 224750.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34175/cc58f91e-c54f-0d23-ca10-1d36ace782b5.png)
![スクリーンショット 2022-11-26 224814.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34175/7a7f5523-8443-4f9e-21f5-fe049d9a0497.png)


その次の Asset ID というのは空で良い（これは FA1.2 ではなく FA2 準拠のトークンの際に利用される）。

さて、それができたら次にトークンの名前や単位を決めていく。これは好きに決めて良いので、分かりやすい名前を付ければ良いと思う。

![スクリーンショット 2022-11-26 224915.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34175/471f11ec-144f-8dea-f97a-8b009b6cc491.png)

これだけで準備は完了である。

トークンの追加が完了すると、アセット一覧に自作トークンが加えられているのが分かるだろう。そして、デプロイ時の初期値設定が上手くいっているならば、既に最初に決めた値分トークンを保有していることになっているはずだ。

![スクリーンショット 2022-11-26 224945.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34175/dbf2fd0c-92ce-c6a2-65fc-cf2070199682.png)

### 自作トークンを受け渡してみる

スマートコントラクトの初期値で設定したとおり、今はこのアドレスだけが世界で唯一この自作トークンを保有するアカウントだ。
この貴重なトークンを、他のアカウントに送ってみることにしよう。

Temple Walllet はアカウントの追加を行うことができる。
右上のアイコンをクリックして、「アカウントを作成または復元する」を選ぶと新規のアカウントを作ることができる。

![スクリーンショット 2022-11-26 225052.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34175/cd3e8c87-75f2-fa2d-3c5a-3506c1deb7fd.png)

これで新しいアカウントを作り、さらに上でやったのと同じ手順で自作トークンを資産に追加する。
この時、先とは違って、トークンの保有数は0になっているはずだ。

![スクリーンショット 2022-11-26 225224.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34175/30803641-ab7b-e27e-0f73-da180caf0645.png)

では最初のアカウントに戻って、自作トークンを選択して「送る」を押してみよう。
宛先と額とを選択する。宛先は、先ほど作成したアカウントが自動でサジェストされるはずだ。
保有している額以下ならいくらでも送れるが、今回は20トークン分送付してみる。
追加の手数料を選択したら、「送る」を押下する。

![スクリーンショット 2022-11-27 011122.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34175/c298e462-b269-5942-7271-7694de3de94e.png)

確認画面が出てくるので、ネットワークが ghostnet であることを確認して、確定しよう。

操作が発行されて、少し待つと確定する。
送付元のトークン保有量が80に減少し、送付先のトークン保有量が20に増加していることが確認できるだろう。

自分で作ったトークンが、確かにトークンとして使えることが確認できた。

## コントラクトが FA1.2 準拠であることを確認する

さて、自作トークンを FA1.2 に準拠した形で作成し、それを外部ツールから扱うことができた。
しかし、このトークンは本当に FA1.2 準拠なのだろうか？

実は tezos-client にはスマートコントラクトが FA1.2 に準拠しているかどうか確認するコマンドが用意されている。

```bash
$ ./tezos-client check contract myfa12 implements fa1.2
```

このコマンドを打つと、次のような結果が返ってくる。

```bash
Contract $your_contract_address has an FA1.2 interface.
```

どうやら作成したスマートコントラクトはきちんと FA1.2 に準拠していたようだ。

## まとめ

FA1.2 に従って SCaml でスマートコントラクトを作成し、それをデプロイし、外部のツールを利用して操作してみた。
この「外部のツールを利用して」というのが大事なところで、きちんと規格に従うことで、その規格に対して作られたツールの恩恵を受けることができるというわけだ。

今回作成したトークンは何の特殊なギミックも無く、現状では何の付加価値も無いが、ブロックチェーンの力で全ての取引が記録されるという強力な性質を持っている。そういうトークンをこれほど簡単に作成できるのは、特筆すべきだろう。