LINE Blockchain APIでサービストークンを操作する

# はじめに

[前回](https://qiita.com/bathtimefish/items/3d2cb04dda2d32f514e5)の続きです。[LINE Blockchain Developers SDK for JavaScript](https://docs-blockchain.line.biz/ja/sdk/javascript-guide)でサービストークンを取り扱ってみます。

前回のソースコードからAPI SECRETなどは[dotenv](https://www.npmjs.com/package/dotenv)に移行しました。

`.env`は以下のように記述します

```.env
BASE_URL='https://test-api.blockchain.line.me'
SERVICE_API_KEY='[Your API Key]'
SERVICE_API_SECRET='[Your API Secret]'
OWNER_WALLET_ADDRESS='[Your Owner Wallet Address]'
OWNER_WALLET_SECRET='[Your Owner Wallet Secret]'
```

# サービストークンを鋳造する

サービストークンを鋳造して指定のウォレットに発行します。まずは指定するウォレットの残高を確認しましょう。

```src/index.ts
import * as dotenv from 'dotenv';
import * as devSdk from '@line/lbd-sdk-js';
import {
    ServiceToken,
    WalletResponse,
    MintServiceTokenRequest,
    TxHashResponse,
    ServiceTokenBalance
} from '@line/lbd-sdk-js';

dotenv.config();

const httpClient = new devSdk.HttpClient(
        process.env.BASE_URL as string,
        process.env.SERVICE_API_KEY as string,
        process.env.SERVICE_API_SECRET as string
    );

const getServiceToken = async (): Promise<ServiceToken> => {
  const srvs = await httpClient.serviceTokens();
  if (!srvs.responseData || srvs.responseData?.length < 1) throw new Error('No response data');
  const results = srvs.responseData;
  if (!results || results.length < 1) throw new Error('No service tokens');
  return results[0];
};

const getWallets = async (): Promise<WalletResponse[]> => {
  const wallets = await httpClient.wallets()
  if (!wallets.responseData || wallets.responseData?.length < 1) throw new Error('No response data');
  const result = wallets.responseData;
  if (!result) throw new Error('No wallet');
  return result;
};

const getWalletByName = (name: string, wallets: WalletResponse[]): WalletResponse|undefined => {
  return wallets.find((w) => { return w.name === name });
};

const getBalanceOfService = async (address: string, contructId: string): Promise<ServiceTokenBalance> => {
  const balance = await httpClient.serviceTokenBalanceOfWallet(address, contructId);
  if (!balance || !balance.responseData) throw new Error('No response data');
  const result = balance.responseData;
  if (!result) throw new Error('No balance data');
  return result;
};

const main = async () => {
  const st = await getServiceToken();
  const wallets = await getWallets();
  const wallet = getWalletByName('bathtimefish', wallets);
  if (!wallet) throw new Error('Wallet not found');
  const bal = await getBalanceOfService(wallet.walletAddress, st.contractId);
  console.log(bal);
};

main();
```

`bathtimefish`ウォレットの残高は`10000`のようです

```
yarn run ts-node src/index.ts
{
  contractId: '[contruct ID]',
  name: 'BTFCoin',
  symbol: 'BTF',
  imgUri: '[image icon url]',
  decimals: 6,
  amount: '10000'
}
```

`mintServiceToken()`を追加してBTFCoinを`1000`鋳造して`bathtimefish`ウォレットに発行します

```src/index.ts
s dotenv from 'dotenv';
import * as devSdk from '@line/lbd-sdk-js';
import {
    ServiceToken,
    WalletResponse,
    MintServiceTokenRequest,
    TxHashResponse,
    ServiceTokenBalance
} from '@line/lbd-sdk-js';

dotenv.config();

const httpClient = new devSdk.HttpClient(
        process.env.BASE_URL as string,
        process.env.SERVICE_API_KEY as string,
        process.env.SERVICE_API_SECRET as string
    );

const getServiceToken = async (): Promise<ServiceToken> => {
  const srvs = await httpClient.serviceTokens();
  if (!srvs.responseData || srvs.responseData?.length < 1) throw new Error('No response data');
  const results = srvs.responseData;
  if (!results || results.length < 1) throw new Error('No service tokens');
  return results[0];
};

const mintServiceToken =async ( contructId: string, req: MintServiceTokenRequest): Promise<TxHashResponse> => {
  const res = await httpClient.mintServiceToken(contructId, req);
  const result = res.responseData;
  if (!result) throw new Error('Error');
  return result;
};

const getWallets = async (): Promise<WalletResponse[]> => {
  const wallets = await httpClient.wallets()
  if (!wallets.responseData || wallets.responseData?.length < 1) throw new Error('No response data');
  const result = wallets.responseData;
  if (!result) throw new Error('No wallet');
  return result;
};

const getWalletByName = (name: string, wallets: WalletResponse[]): WalletResponse|undefined => {
  return wallets.find((w) => { return w.name === name });
};

const getBalanceOfService = async (address: string, contructId: string): Promise<ServiceTokenBalance> => {
  const balance = await httpClient.serviceTokenBalanceOfWallet(address, contructId);
  if (!balance || !balance.responseData) throw new Error('No response data');
  const result = balance.responseData;
  if (!result) throw new Error('No balance data');
  return result;
};

const main = async () => {
  const st = await getServiceToken();
  console.log(st);
  const wallets = await getWallets();
  const wallet = getWalletByName('bathtimefish', wallets);
  if (!wallet) throw new Error('Wallet not found');
  const bal = await getBalanceOfService(wallet.walletAddress, st.contractId);
  console.log(bal);
  const mintRequest: MintServiceTokenRequest = {
    toAddress: wallet.walletAddress,
    amount: '1000',
    ownerAddress: process.env.OWNER_WALLET_ADDRESS as string,
    ownerSecret: process.env.OWNER_WALLET_SECRET as string,
  };
  const mintResult = await mintServiceToken(st.contractId, mintRequest);
  console.log(mintRequest);
};

main();
```

`mintResult`を確認すると正常に処理できた感じです

```
yarn run ts-node src/index.ts
{
  toAddress: '[To Wallet Address]',
  amount: '1000',
  ownerAddress: '[Owner Wallet Address]',
  ownerSecret: '[Owner Wallet Secret]'
}
```

結果は即時反映されないようなので、すこし経ってからもう一度残高を確認します

```src/index.ts
import * as dotenv from 'dotenv';
import * as devSdk from '@line/lbd-sdk-js';
import {
    ServiceToken,
    WalletResponse,
    MintServiceTokenRequest,
    TxHashResponse,
    ServiceTokenBalance
} from '@line/lbd-sdk-js';

dotenv.config();

const httpClient = new devSdk.HttpClient(
        process.env.BASE_URL as string,
        process.env.SERVICE_API_KEY as string,
        process.env.SERVICE_API_SECRET as string
    );

const getServiceToken = async (): Promise<ServiceToken> => {
  const srvs = await httpClient.serviceTokens();
  if (!srvs.responseData || srvs.responseData?.length < 1) throw new Error('No response data');
  const results = srvs.responseData;
  if (!results || results.length < 1) throw new Error('No service tokens');
  return results[0];
};

const mintServiceToken =async ( contructId: string, req: MintServiceTokenRequest): Promise<TxHashResponse> => {
  const res = await httpClient.mintServiceToken(contructId, req);
  const result = res.responseData;
  if (!result) throw new Error('Error');
  return result;
};

const getWallets = async (): Promise<WalletResponse[]> => {
  const wallets = await httpClient.wallets()
  if (!wallets.responseData || wallets.responseData?.length < 1) throw new Error('No response data');
  const result = wallets.responseData;
  if (!result) throw new Error('No wallet');
  return result;
};

const getWalletByName = (name: string, wallets: WalletResponse[]): WalletResponse|undefined => {
  return wallets.find((w) => { return w.name === name });
};

const getBalanceOfService = async (address: string, contructId: string): Promise<ServiceTokenBalance> => {
  const balance = await httpClient.serviceTokenBalanceOfWallet(address, contructId);
  if (!balance || !balance.responseData) throw new Error('No response data');
  const result = balance.responseData;
  if (!result) throw new Error('No balance data');
  return result;
};

const main = async () => {
  const st = await getServiceToken();
  console.log(st);
  const wallets = await getWallets();
  const wallet = getWalletByName('bathtimefish', wallets);
  if (!wallet) throw new Error('Wallet not found');
  const bal = await getBalanceOfService(wallet.walletAddress, st.contractId);
  console.log(bal);
};

main();
```

残高が`11000`に増えました

```
yarn run ts-node src/index.ts
{
  contractId: '[contruct ID]',
  name: 'BTFCoin',
  symbol: 'BTF',
  imgUri: '[image icon url]',
  decimals: 6,
  amount: '11000'
}
```

# サービストークンを転送する

サービストークンを他のウォレットに転送します。ここではあらかじめ転送先の`Alice`ウォレットをLINE Blockchain Developersコンソールで作成しておきます

まず、`Alice`ウォレットの残高を確認します

```src/index.ts
rt * as dotenv from 'dotenv';
import * as devSdk from '@line/lbd-sdk-js';
import {
    ServiceToken,
    WalletResponse,
    ServiceTokenBalance
} from '@line/lbd-sdk-js';

dotenv.config();

const httpClient = new devSdk.HttpClient(
        process.env.BASE_URL as string,
        process.env.SERVICE_API_KEY as string,
        process.env.SERVICE_API_SECRET as string
    );

const getServiceToken = async (): Promise<ServiceToken> => {
  const srvs = await httpClient.serviceTokens();
  if (!srvs.responseData || srvs.responseData?.length < 1) throw new Error('No response data');
  const results = srvs.responseData;
  if (!results || results.length < 1) throw new Error('No service tokens');
  return results[0];
};

const getWallets = async (): Promise<WalletResponse[]> => {
  const wallets = await httpClient.wallets()
  if (!wallets.responseData || wallets.responseData?.length < 1) throw new Error('No response data');
  const result = wallets.responseData;
  if (!result) throw new Error('No wallet');
  return result;
};

const getWalletByName = (name: string, wallets: WalletResponse[]): WalletResponse|undefined => {
  return wallets.find((w) => { return w.name === name });
};

const getBalanceOfService = async (address: string, contructId: string): Promise<ServiceTokenBalance> => {
  const balance = await httpClient.serviceTokenBalanceOfWallet(address, contructId);
  if (!balance || !balance.responseData) throw new Error('No response data');
  const result = balance.responseData;
  if (!result) throw new Error('No balance data');
  return result;
};

const main = async () => {
  const st = await getServiceToken();
  const wallets = await getWallets();
  const ownerWallet = getWalletByName('bathtimefish', wallets);
  const aliceWallet = getWalletByName('Alice', wallets);
  if (!ownerWallet) throw new Error('Wallet not found');
  if (!aliceWallet) throw new Error('Wallet not found');
  const bal = await getBalanceOfService(aliceWallet.walletAddress, st.contractId);
  console.log(bal);
};

main();
```

`Alice`の残高は`0`です

```
yarn run ts-node src/index.ts
{
  contractId: '[contruct ID]',
  name: 'BTFCoin',
  symbol: 'BTF',
  imgUri: '[image icon url]',
  decimals: 6,
  amount: '0'
}
```

`bathtimefish`から`Alice`へサービストークン`1000`を転送します

```src/index.ts
import * as dotenv from 'dotenv';
import * as devSdk from '@line/lbd-sdk-js';
import {
    ServiceToken,
    WalletResponse,
    ServiceTokenBalance,
    TransferServiceTokenRequest,
} from '@line/lbd-sdk-js';

dotenv.config();

const httpClient = new devSdk.HttpClient(
        process.env.BASE_URL as string,
        process.env.SERVICE_API_KEY as string,
        process.env.SERVICE_API_SECRET as string
    );

const getServiceToken = async (): Promise<ServiceToken> => {
  const srvs = await httpClient.serviceTokens();
  if (!srvs.responseData || srvs.responseData?.length < 1) throw new Error('No response data');
  const results = srvs.responseData;
  if (!results || results.length < 1) throw new Error('No service tokens');
  return results[0];
};

const getWallets = async (): Promise<WalletResponse[]> => {
  const wallets = await httpClient.wallets()
  if (!wallets.responseData || wallets.responseData?.length < 1) throw new Error('No response data');
  const result = wallets.responseData;
  if (!result) throw new Error('No wallet');
  return result;
};

const getWalletByName = (name: string, wallets: WalletResponse[]): WalletResponse|undefined => {
  return wallets.find((w) => { return w.name === name });
};

const getBalanceOfService = async (address: string, contructId: string): Promise<ServiceTokenBalance> => {
  const balance = await httpClient.serviceTokenBalanceOfWallet(address, contructId);
  if (!balance || !balance.responseData) throw new Error('No response data');
  const result = balance.responseData;
  if (!result) throw new Error('No balance data');
  return result;
};

const transferServiceToken = async (walletAddress: string, contractId: string, request: TransferServiceTokenRequest) => {
  const res = await httpClient.transferServiceTokenOfWallet(walletAddress, contractId, request);
  if (!res || !res.responseData) throw new Error('No response data');
  const result = res.responseData;
  if (!result) throw new Error('No balance data');
  return result;
}

const main = async () => {
  const st = await getServiceToken();
  const wallets = await getWallets();
  const ownerWallet = getWalletByName('bathtimefish', wallets);
  const aliceWallet = getWalletByName('Alice', wallets);
  if (!ownerWallet) throw new Error('Wallet not found');
  if (!aliceWallet) throw new Error('Wallet not found');
  const transferRequest: TransferServiceTokenRequest = {
    walletSecret: process.env.OWNER_WALLET_SECRET as string,
    toAddress: aliceWallet.walletAddress,
    amount: '1000',
  };
  const transferResult = await transferServiceToken(ownerWallet.walletAddress, st.contractId, transferRequest);
  console.log(transferRequest);

main();
```

転送できたようです

```
yarn run ts-node src/index.ts
{
  walletSecret: '[Wallet Secret]',
  toAddress: '[To Wallet Address]',
  amount: '1000'
}
```

結果は即時反映されないようなので、すこし経ってからもう一度残高を確認します

```src/index.ts
import * as dotenv from 'dotenv';
import * as devSdk from '@line/lbd-sdk-js';
import {
    ServiceToken,
    WalletResponse,
    ServiceTokenBalance,
    TransferServiceTokenRequest,
} from '@line/lbd-sdk-js';

dotenv.config();

const httpClient = new devSdk.HttpClient(
        process.env.BASE_URL as string,
        process.env.SERVICE_API_KEY as string,
        process.env.SERVICE_API_SECRET as string
    );

const getServiceToken = async (): Promise<ServiceToken> => {
  const srvs = await httpClient.serviceTokens();
  if (!srvs.responseData || srvs.responseData?.length < 1) throw new Error('No response data');
  const results = srvs.responseData;
  if (!results || results.length < 1) throw new Error('No service tokens');
  return results[0];
};

const getWallets = async (): Promise<WalletResponse[]> => {
  const wallets = await httpClient.wallets()
  if (!wallets.responseData || wallets.responseData?.length < 1) throw new Error('No response data');
  const result = wallets.responseData;
  if (!result) throw new Error('No wallet');
  return result;
};

const getWalletByName = (name: string, wallets: WalletResponse[]): WalletResponse|undefined => {
  return wallets.find((w) => { return w.name === name });
};

const getBalanceOfService = async (address: string, contructId: string): Promise<ServiceTokenBalance> => {
  const balance = await httpClient.serviceTokenBalanceOfWallet(address, contructId);
  if (!balance || !balance.responseData) throw new Error('No response data');
  const result = balance.responseData;
  if (!result) throw new Error('No balance data');
  return result;
};

const main = async () => {
  const st = await getServiceToken();
  const wallets = await getWallets();
  const ownerWallet = getWalletByName('bathtimefish', wallets);
  const aliceWallet = getWalletByName('Alice', wallets);
  if (!ownerWallet) throw new Error('Wallet not found');
  if (!aliceWallet) throw new Error('Wallet not found');
  const bal = await getBalanceOfService(aliceWallet.walletAddress, st.contractId);
  console.log(bal);
};

main();
```

残高が増えました

```
{
  contractId: '[contruct ID]',
  name: 'BTFCoin',
  symbol: 'BTF',
  imgUri: '[image icon url]',
  decimals: 6,
  amount: '1000'
}
```

# サービストークンを焼却する

指定したウォレットのサービストークンを焼却します。`bathtimefish`のトークンを`1000`焼却します

```src/index.ts
import * as dotenv from 'dotenv';
import * as devSdk from '@line/lbd-sdk-js';
import {
    ServiceToken,
    WalletResponse,
    ServiceTokenBalance,
    BurnFromServiceTokenRequest,
    TxHashResponse,
} from '@line/lbd-sdk-js';

dotenv.config();

const httpClient = new devSdk.HttpClient(
        process.env.BASE_URL as string,
        process.env.SERVICE_API_KEY as string,
        process.env.SERVICE_API_SECRET as string
    );

const getServiceToken = async (): Promise<ServiceToken> => {
  const srvs = await httpClient.serviceTokens();
  if (!srvs.responseData || srvs.responseData?.length < 1) throw new Error('No response data');
  const results = srvs.responseData;
  if (!results || results.length < 1) throw new Error('No service tokens');
  return results[0];
};

const getWallets = async (): Promise<WalletResponse[]> => {
  const wallets = await httpClient.wallets()
  if (!wallets.responseData || wallets.responseData?.length < 1) throw new Error('No response data');
  const result = wallets.responseData;
  if (!result) throw new Error('No wallet');
  return result;
};

const getWalletByName = (name: string, wallets: WalletResponse[]): WalletResponse|undefined => {
  return wallets.find((w) => { return w.name === name });
};

const getBalanceOfService = async (address: string, contructId: string): Promise<ServiceTokenBalance> => {
  const balance = await httpClient.serviceTokenBalanceOfWallet(address, contructId);
  if (!balance || !balance.responseData) throw new Error('No response data');
  const result = balance.responseData;
  if (!result) throw new Error('No balance data');
  return result;
};

const burnServiceToken = async (contractId: string, request: BurnFromServiceTokenRequest): Promise<TxHashResponse> => {
  const res = await httpClient.burnFromServiceToken(contractId, request);
  if (!res || !res.responseData) throw new Error('No response data');
  const result = res.responseData;
  if (!result) throw new Error('No data');
  return result;
}

const main = async () => {
  const st = await getServiceToken();
  const wallets = await getWallets();
  const wallet = getWalletByName('bathtimefish', wallets);
  if (!wallet) throw new Error('Wallet not found');
  const burnRequest: BurnFromServiceTokenRequest = {
    fromAddress: wallet.walletAddress,
    amount: '1000',
    ownerAddress: process.env.OWNER_WALLET_ADDRESS as string,
    ownerSecret: process.env.OWNER_WALLET_SECRET as string,
  };
  const burnResult = await burnServiceToken(st.contractId, burnRequest);
  console.log(burnRequest);
};

main();
```

焼却できたようです

```
{
  fromAddress: '[From Wallet Address]',
  amount: '1000',
  ownerAddress: '[Owner Wallet Address]',
  ownerSecret: '[Owner Wallet Secret]'
}
```

結果は即時反映されないようなので、すこし経ってからもう一度残高を確認します

```src/index.ts
import * as dotenv from 'dotenv';
import * as devSdk from '@line/lbd-sdk-js';
import {
    ServiceToken,
    WalletResponse,
    ServiceTokenBalance,
    BurnFromServiceTokenRequest,
    TxHashResponse,
} from '@line/lbd-sdk-js';

dotenv.config();

const httpClient = new devSdk.HttpClient(
        process.env.BASE_URL as string,
        process.env.SERVICE_API_KEY as string,
        process.env.SERVICE_API_SECRET as string
    );

const getServiceToken = async (): Promise<ServiceToken> => {
  const srvs = await httpClient.serviceTokens();
  if (!srvs.responseData || srvs.responseData?.length < 1) throw new Error('No response data');
  const results = srvs.responseData;
  if (!results || results.length < 1) throw new Error('No service tokens');
  return results[0];
};

const getWallets = async (): Promise<WalletResponse[]> => {
  const wallets = await httpClient.wallets()
  if (!wallets.responseData || wallets.responseData?.length < 1) throw new Error('No response data');
  const result = wallets.responseData;
  if (!result) throw new Error('No wallet');
  return result;
};

const getWalletByName = (name: string, wallets: WalletResponse[]): WalletResponse|undefined => {
  return wallets.find((w) => { return w.name === name });
};

const getBalanceOfService = async (address: string, contructId: string): Promise<ServiceTokenBalance> => {
  const balance = await httpClient.serviceTokenBalanceOfWallet(address, contructId);
  if (!balance || !balance.responseData) throw new Error('No response data');
  const result = balance.responseData;
  if (!result) throw new Error('No balance data');
  return result;
};

const burnServiceToken = async (contractId: string, request: BurnFromServiceTokenRequest): Promise<TxHashResponse> => {
  const res = await httpClient.burnFromServiceToken(contractId, request);
  if (!res || !res.responseData) throw new Error('No response data');
  const result = res.responseData;
  if (!result) throw new Error('No data');
  return result;
}

const main = async () => {
  const st = await getServiceToken();
  const wallets = await getWallets();
  const wallet = getWalletByName('bathtimefish', wallets);
  if (!wallet) throw new Error('Wallet not found');
  const bal = await getBalanceOfService(wallet.walletAddress, st.contractId);
  console.log(bal);
};

main();
```

残高が減りました

```
{
  contractId: '[contruct ID]',
  name: 'BTFCoin',
  symbol: 'BTF',
  imgUri: '[image icon url]',
  decimals: 6,
  amount: '9000'
}
```

# おわりに

サービストークンは[ERC-20](https://ethereum.org/ja/developers/docs/standards/tokens/erc-20/)規格に似ているため仮想通貨のようなものを作ることができます。LINE BrockchainのJavaScript SDKを使って上記のように簡単に扱うことができるので仮想通貨を初学者は勉強に使のもいいかもしれません。