Stellarブロックチェーンで独自トークン(Qiitaトークン)を管理してみる

こんにちは。
最近メインをRubyからTypescriptに移し、新しいことだらけで新鮮なエンジニアライフを過ごしているshunichiです。Typescriptに触れるタイミングと同時にブロックチェーンにも触れる機会をいただいたので久々に投稿してみました。

# はじめに
ブロックチェーン技術の１つであるStellarを用いて、独自トークンを運用する機会があったので基本的なことをメモ代わりとしてまとめました。
Stellarはビットフライヤーでも扱われているので、ある程度知名度が高い仮想通貨になるかと思います。
![スクリーンショット 2021-12-20 10.53.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239829/1cc1d9dd-6305-cdcb-82b5-a91bab66bb17.png)

ここではQiitaトークンの発行及び、そのトークンの受け渡しに関する具体的な処理を記載しています。

# Stellarの特徴
https://recruit.gmo.jp/engineer/jisedai/blog/stellar_blockchain_stellar_token/

こちらの記事でStellarについて詳しく書いてあります。この記事を引用すると特徴は大きく４つ。

> ・スマートコントラクトの簡易作成及び簡易修正、更新
> ・トラストライン（Trust Line）の概念を導入
> ・アセット（トークン）の簡易作成、発行
> ・アカウントレベルのコントロール

費用に対するメリットもあります。

- 送金に関して処理が早く、手数料が安い
- 0.00001円くらいの手数料
- 他のブロックチェーンだと数百円とか

### 取引ルール

※括弧内はStellar用語

1. お互いが口座(`account`)を持つこと
2. トークン受け取りの許可(`trustline`)がされていること
3. お金もしくはトークン（`assets`）の受け渡しを取引（`transaction`）とし、一つ一つの取引に複数の操作（`operation`）が可能であること 

`トラストラインの概念`と`アセットの発行`を理解すれば簡単に独自トークンが発行できます。  
Stellarの取引のポイントとしては、トークン受け取りの許可は、受け取り主から受け渡し元に、”**あなたが発行している○◯トークンを受け付けます**”みたいな感じで、事前に宣言する”取引”が必要になることです。

### アカウント種別

- Issuer：　トークンの発行元
- Distributor: トークンの主な付与元(Issuerが発行したトークンを受け取り、Endに配布する)
- End：　各アカウント(ユーザやクライアントなど)
- Admin： Issuer, Distributorを管理

サービス内で必要になってくるのは基本的に大きく4種類。EndとAdminの必要性については各サービスによって変わってくると思います。

トークンの流れはざっくりこんな感じ。
Issuer → Distributor → End ←→ END

ちなみに独自トークンは**無制限**に発行可能です。 
ただ簡単にトークンを発行されては困るので、運用時はIssuerとDistributorのアカウントをハードウォレット管理するのがいいみたいです。ハードウォレット管理についてはこの記事の範囲外なので記載しませんが、詳しくはこの記事が参考になると思います。

https://hardwarewallet.jp/ledger/stellar.html

# Stellarを使ってみる

テスト環境でのStellarアカウントの作成には画面上で簡単に作成できます。

### アカウント作成
https://laboratory.stellar.org/#account-creator?network=test

右上のタブが環境を表していて、今回はtestタブを選択した状態で`Generate keypair`をクリックし公開鍵と秘密鍵を発行します。公開鍵と秘密鍵は後々必要になるのでメモしておきましょう。
![スクリーンショット 2021-12-07 22.37.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239829/0820eab4-16f5-944b-a175-a33a3b768b76.png)

`Friendbot: Func a test network account`の入力部分に公開鍵を入力し、`Get test network lumens`をクリックするとアカウントが作成されます。
![スクリーンショット 2021-12-07 22.38.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239829/ae54cc02-53b9-20c1-8134-2f7cca8235ae.png)


アカウント作成後、下記ページで公開鍵からアカウント情報が閲覧できます。
https://laboratory.stellar.org/#explorer?resource=accounts&endpoint=single&network=test
※テスト環境で発行したアカウントは2~3ヶ月くらいの間隔で定期的に削除されます

テスト環境だと10000XLM付与されているのが確認できます。

```json:アカウント情報
{
  "_links": {
    "self": {
      "href": "https://horizon-testnet.stellar.org/accounts/GDDH2FIR6OKAZHI4DLZULLORSJNCJJUNSN2GRL7OV4PJRXTIL4K3PJKB"
    },
    "transactions": {
      "href": "https://horizon-testnet.stellar.org/accounts/GDDH2FIR6OKAZHI4DLZULLORSJNCJJUNSN2GRL7OV4PJRXTIL4K3PJKB/transactions{?cursor,limit,order}",
      "templated": true
    },
    "operations": {
      "href": "https://horizon-testnet.stellar.org/accounts/GDDH2FIR6OKAZHI4DLZULLORSJNCJJUNSN2GRL7OV4PJRXTIL4K3PJKB/operations{?cursor,limit,order}",
      "templated": true
    },
    "payments": {
      "href": "https://horizon-testnet.stellar.org/accounts/GDDH2FIR6OKAZHI4DLZULLORSJNCJJUNSN2GRL7OV4PJRXTIL4K3PJKB/payments{?cursor,limit,order}",
      "templated": true
    },
    "effects": {
      "href": "https://horizon-testnet.stellar.org/accounts/GDDH2FIR6OKAZHI4DLZULLORSJNCJJUNSN2GRL7OV4PJRXTIL4K3PJKB/effects{?cursor,limit,order}",
      "templated": true
    },
    "offers": {
      "href": "https://horizon-testnet.stellar.org/accounts/GDDH2FIR6OKAZHI4DLZULLORSJNCJJUNSN2GRL7OV4PJRXTIL4K3PJKB/offers{?cursor,limit,order}",
      "templated": true
    },
    "trades": {
      "href": "https://horizon-testnet.stellar.org/accounts/GDDH2FIR6OKAZHI4DLZULLORSJNCJJUNSN2GRL7OV4PJRXTIL4K3PJKB/trades{?cursor,limit,order}",
      "templated": true
    },
    "data": {
      "href": "https://horizon-testnet.stellar.org/accounts/GDDH2FIR6OKAZHI4DLZULLORSJNCJJUNSN2GRL7OV4PJRXTIL4K3PJKB/data/{key}",
      "templated": true
    }
  },
  "id": "GDDH2FIR6OKAZHI4DLZULLORSJNCJJUNSN2GRL7OV4PJRXTIL4K3PJKB",
  "account_id": "GDDH2FIR6OKAZHI4DLZULLORSJNCJJUNSN2GRL7OV4PJRXTIL4K3PJKB",
  "sequence": "5873492331331584",
  "subentry_count": 0,
  "last_modified_ledger": 1367529,
  "last_modified_time": "2021-12-07T13:38:20Z",
  "thresholds": {
    "low_threshold": 0,
    "med_threshold": 0,
    "high_threshold": 0
  },
  "flags": {
    "auth_required": false,
    "auth_revocable": false,
    "auth_immutable": false,
    "auth_clawback_enabled": false
  },
  "balances": [
    {
      "balance": "10000.0000000",
      "buying_liabilities": "0.0000000",
      "selling_liabilities": "0.0000000",
      "asset_type": "native"
    }
  ],
  "signers": [
    {
      "weight": 1,
      "key": "GDDH2FIR6OKAZHI4DLZULLORSJNCJJUNSN2GRL7OV4PJRXTIL4K3PJKB",
      "type": "ed25519_public_key"
    }
  ],
  "data": {},
  "num_sponsoring": 0,
  "num_sponsored": 0,
  "paging_token": "GDDH2FIR6OKAZHI4DLZULLORSJNCJJUNSN2GRL7OV4PJRXTIL4K3PJKB"
}
```

ここからはコードベースで発行する手順

- axiosを使うパターン

```typescript:アカウント作成
import axios from "axios";
import { Keypair } from "stellar-sdk";

async function createAccount() {
  try {
    const keyPair = Keypair.random();
    await axios.get(`https://friendbot.stellar.org?addr=${encodeURIComponent(keyPair.publicKey())}`);
  } catch (e) {
    console.log(e);
  }
}
```

- Stellar Transactionを使うパターン

```typescript:アカウント作成
import * as StellarSdk from "stellar-sdk";
import { Keypair, Memo, Operation, Networks, TransactionBuilder } from "stellar-sdk";

async createAccount() {
  try {
    const keyPair = Keypair.random();
    const secretKey = keyPair.secret();
    const publicKey = keyPari.publickKey();
    const stellarServer = StellarSdk.Server("stellar　network url");
    const account = await new stellarServer.loadAccount(publicKey);

    const transaction = new TransactionBuilder(
      sourceAccount: account,
      options: {
        fee: 100, // Transactionごとの最大支払い数
        newworkPassphrase: Networks.TESNET // PUBLIC: Public Global Stellar Network, TESTNET: Test SDF Network
      }
    )
      .addOperation(
        Operation.createAccount({
          destination: publicKey,
          startingBalance: 100 // native asset
        })
      )
      .addMemo(Memo.text("Create Account"))
      .setTimeout(300)
      .build();

    transaction.sign(keyPair);
    await stellarServer.submitTransaction(transaction);
  } catch (e) {
    console.log(e);
  }
}
```

`sign`メソッドにKeypairオブジェクトを渡してトランザクションに対して秘密鍵で電子署名し、信頼性を担保した上でHorizon経由でStellarネットワークに対してブロードキャストします。

### トラストライン

Issuerが発行したQiitaトークンを受け取れるようにトラストラインを確立します。

```typescript:トラストライン確立
import * as StellarSdk from "stellar-sdk";
import { Keypair, Asset, Operation, Networks, TransactionBuilder } from "stellar-sdk";

async createTrustLine(secretKey: string) {
  try {
    const keyPair = Keypair.fromSecret(secretKey);
    const stellarServer = StellarSdk.Server("stellar　network url");
    const account = await new stellarServer.loadAccount(keyPair.publicKey());
    const qiitaAsset = new Asset("Qiita", "Issuerの公開鍵");

    const transaction = new TransactionBuilder(
      sourceAccount: account,
      options: { fee: 100, newworkPassphrase: Networks.TESNET }
    )
      .addOperation(
        Operation.changeTrust({ asset: qiitaAsset })
      )
      .setTimeout(300)
      .build();

    transaction.sign(keyPair);
    stellarServer.submitTransaction(transaction);
  } catch (e) {
    console.log(e);
  }
}
```

上記処理のトランザクションがサブミットされるとQiitaトークンの受付が許可された状態になります

```json:アカウント情報
{
  "_links": {
    "self": {
      "href": "https://horizon-testnet.stellar.org/accounts/GDNVDPBKFQPGNV4IQPKB7G6LNHYEIRTYUVNSZG3Y36R47XSWUXMLGDGU"
    },
    "transactions": {
      "href": "https://horizon-testnet.stellar.org/accounts/GDNVDPBKFQPGNV4IQPKB7G6LNHYEIRTYUVNSZG3Y36R47XSWUXMLGDGU/transactions{?cursor,limit,order}",
      "templated": true
    },
    "operations": {
      "href": "https://horizon-testnet.stellar.org/accounts/GDNVDPBKFQPGNV4IQPKB7G6LNHYEIRTYUVNSZG3Y36R47XSWUXMLGDGU/operations{?cursor,limit,order}",
      "templated": true
    },
    "payments": {
      "href": "https://horizon-testnet.stellar.org/accounts/GDNVDPBKFQPGNV4IQPKB7G6LNHYEIRTYUVNSZG3Y36R47XSWUXMLGDGU/payments{?cursor,limit,order}",
      "templated": true
    },
    "effects": {
      "href": "https://horizon-testnet.stellar.org/accounts/GDNVDPBKFQPGNV4IQPKB7G6LNHYEIRTYUVNSZG3Y36R47XSWUXMLGDGU/effects{?cursor,limit,order}",
      "templated": true
    },
    "offers": {
      "href": "https://horizon-testnet.stellar.org/accounts/GDNVDPBKFQPGNV4IQPKB7G6LNHYEIRTYUVNSZG3Y36R47XSWUXMLGDGU/offers{?cursor,limit,order}",
      "templated": true
    },
    "trades": {
      "href": "https://horizon-testnet.stellar.org/accounts/GDNVDPBKFQPGNV4IQPKB7G6LNHYEIRTYUVNSZG3Y36R47XSWUXMLGDGU/trades{?cursor,limit,order}",
      "templated": true
    },
    "data": {
      "href": "https://horizon-testnet.stellar.org/accounts/GDNVDPBKFQPGNV4IQPKB7G6LNHYEIRTYUVNSZG3Y36R47XSWUXMLGDGU/data/{key}",
      "templated": true
    }
  },
  "id": "GDNVDPBKFQPGNV4IQPKB7G6LNHYEIRTYUVNSZG3Y36R47XSWUXMLGDGU",
  "account_id": "GDNVDPBKFQPGNV4IQPKB7G6LNHYEIRTYUVNSZG3Y36R47XSWUXMLGDGU",
  "sequence": "82008105549825",
  "subentry_count": 1,
  "last_modified_ledger": 19120,
  "last_modified_time": "2021-12-16T13:21:33Z",
  "thresholds": {
    "low_threshold": 0,
    "med_threshold": 0,
    "high_threshold": 0
  },
  "flags": {
    "auth_required": false,
    "auth_revocable": false,
    "auth_immutable": false,
    "auth_clawback_enabled": false
  },
  "balances": [
    {
      "balance": "0.0000000",
      "limit": "922337203685.4775807",
      "buying_liabilities": "0.0000000",
      "selling_liabilities": "0.0000000",
      "last_modified_ledger": 19120,
      "is_authorized": true,
      "is_authorized_to_maintain_liabilities": true,
      "asset_type": "credit_alphanum12",
      "asset_code": "Qiita",
      "asset_issuer": "GBFZ4LDR4ONXDLCWQMAHZNE5EATNJCOESOMEQIAWRFA6FTC7SRVVS46D"
    },
    {
      "balance": "9999.9999900",
      "buying_liabilities": "0.0000000",
      "selling_liabilities": "0.0000000",
      "asset_type": "native"
    }
  ],
  "signers": [
    {
      "weight": 1,
      "key": "GDNVDPBKFQPGNV4IQPKB7G6LNHYEIRTYUVNSZG3Y36R47XSWUXMLGDGU",
      "type": "ed25519_public_key"
    }
  ],
  "data": {},
  "num_sponsoring": 0,
  "num_sponsored": 0,
  "paging_token": "GDNVDPBKFQPGNV4IQPKB7G6LNHYEIRTYUVNSZG3Y36R47XSWUXMLGDGU"
}
```


### 送金/払い込み

```typescript
import * as StellarSdk from "stellar-sdk";
import { Keypair, Memo, Asset, Operation, Networks, TransactionBuilder } from "stellar-sdk";

async paymentToken(amount: string) {
  try {
    const stellarServer = StellarSdk.Server("stellar　network url");
    const qiitaAsset = new Asset("Qiita", "Issuerの公開鍵");
    const sourceAccount = await new stellarServer.loadAccount("送付元の公開鍵");
    const sourceKeyPair = Keypair.fromSecret("送付元の秘密鍵")
    const destinationAccount = await new stellarServer.loadAccount("送付先の公開鍵");
    const transaction = new TransactionBuilder(
      sourceAccount,
      options: { fee: 100, newworkPassphrase: Networks.TESNET }
    )
    .addOperation(
      Operation.payment({
        destination: destinationAccount.publicKey,
        asset: qiitaAsset,
        amount: amount,
      })
    )
    .addMemo(Memo.text("payment qiita token"))
    .setTimeout(300)
    .build();
  
    transaction.sign(sourceKeyPair);
    await stellarServer.submitTransaction(transaction);
  } catch (e) {
    console.log(e);
  }
}
```

Assetの送金は、Issuerから任意のアカウントへの送金が無制限に可能です。
他アカウントがQiitaトークンを送付する場合は、そのアカウントの保有数のみが送金可能になります。

# まとめ
- トークン発行: `Asset("トークン名", "Issuerの公開鍵")`
- アカウント作成：　`Operation.createAccount`
- トラストラインの確立： `Operation.changeTrust`
- 送金： `Operation.payment`

大きく4つの役割が使えれば、ブロックチェーンが使えます。
記載のソースコードは役割ごとに同じ処理を書いている部分が多いので、リファクタするとかなりスッキリすると思います。

他にもトークンの凍結や取り戻し(クローバッグ)、トークンバーニングなどさまざまな機能があるので
より深く学びたい場合はディベロッパーガイドに分かりやすく記載されているのでご覧になってみてください。

# 参考
https://developers.stellar.org/docs/issuing-assets/

https://crypto.watch.impress.co.jp/docs/serial/dmmcourse/1159808.html

https://stellar.expert/explorer/public

トラストラインや送金を画面上から行う方法について記載

https://nandovieira.com/creating-custom-assets-with-stellar