LINE Blockchain APIでアイテムトークンを操作する その1

# はじめに
[前回](https://qiita.com/bathtimefish/items/35ed048b5200a5d57fd7)の続きです。[LINE Blockchain Developers SDK for JavaScript](https://docs-blockchain.line.biz/ja/sdk/javascript-guide)でアイテムトークンを取り扱ってみます。流行りに乗って[Non-fungibleアイテムトークン](https://docs.blockchain.line.me/ja/glossary/#non-fungible-item-token)を発行してみましょう。

アイテムトークンに画像や動画を登録するルールについては[アイテムトークンのアイコンを提供する](https://docs.blockchain.line.me/ja/how-to/provide-item-token-images)に詳しく解説されています。LINE BITMAX Walletなどでアイテムトークンを使用する場合は参考にしてください。

# アイテムトークンコントラクトを作成する
アイテムトークンを鋳造するには、[アイテムトークンコントラクト](https://docs.blockchain.line.me/ja/glossary/#contract-id)をが必要です。アイテムトークンコントラクトIDは[LINE Blockchainサービス登録時](https://qiita.com/bathtimefish/items/86991137a0752c93619d)に作成されていますが、コントラクトの情報が未登録なので登録していきます。

まず、LINE Blockchain Developersのコンソールから該当するBlockchainサービスの`Settings`を選択して`Base URI of token icon`にアイテムトークンコントラクトの画像／動画を格納するURIを設定します。今回はAWS S3で適当な公開用バケットを作成して、そのURLを設定しました

欄下の✓マークをクリックしWallet Secretを入力してOKをクリックします

![スクリーンショット 2022-10-02 14.41.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61995/e966d7cd-05db-7f5c-21f6-ec999ee5ccea.png)

登録が完了すると以下のようになります

![スクリーンショット 2022-10-02 14.41.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61995/1588374c-aff8-41e1-712b-0bd8929f55bd.png)

# アイテムトークンコントラクトを取得する

[アイテムトークンのコントラクトを取得](https://docs.blockchain.line.me/ja/api-guide/category-item-tokens/retrieve#v1-item-tokens-contractId-get)して確認します

```src/index.ts
import * as dotenv from 'dotenv';
import * as devSdk from '@line/lbd-sdk-js';
import {
    WalletResponse,
    ItemToken,
} 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 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 getItemTokenContract = async (contractId: string): Promise<ItemToken|undefined> => {
  const res = await httpClient.itemToken(contractId);
  return res.responseData;
}

const main = async () => {
  const wallets = await getWallets();
  const btfWallet = getWalletByName('bathtimefish', wallets);
  if (!btfWallet) throw new Error('Wallet not found');
  const contract = await getItemTokenContract(process.env.ITEM_TOKEN_CONTRACT_ID as string);
  console.log(contract);
};

main();
```

実行すると以下のようにアイテムトークンコントラクトの情報を得ることができます

```shell
yarn run ts-node src/index.ts
{
  contractId: '[ITEM TOKEN CONTRACT ID]',
  baseImgUri: 'https://btf-lineblockchainapi.s3.ap-northeast-3.amazonaws.com/',
  ownerAddress: '[OWNER WALLET ADDRESS]',
  createdAt: 1660481980316,
  serviceId: '[SERVICE ID]'
}
✨  Done in 1.95s.
```

# Non-fungibleアイテムトークンを発行する

LINE Blockchain Developersのコンソールから該当するBlockchainサービスの`Asset`を選択します

![Create-item-token-contract.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61995/7ef8d311-9ea8-1705-e9c8-00a6bc72f811.png)

`Item token(0)`の`Issue New`をクリックし、下図のように`Non-fungible`を選択、`Token name`欄にアイテムトークン名を入力します

![スクリーンショット 2022-10-02 14.56.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61995/be5f2bd8-e9e8-f09f-a273-81c3a8afc40d.png)

`Issue`をクリックし、Wallet Secretを入力してOKをクリックします

![スクリーンショット 2022-10-02 14.58.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61995/1477bd31-4cc9-1e3f-7ce4-3d65006dc9bf.png)

確認画面が表示されるので`Confirm`をクリックします

![スクリーンショット 2022-10-02 15.00.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61995/b04f4256-3d9f-334b-4535-55e64b7bbfc5.png)

完了すると以下のようにNFTが発行されます

![スクリーンショット 2022-10-02 15.02.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61995/57d4981a-f226-8c24-bc5a-42130f107e67.png)

# Non-Fungibleアイテムトークンを取得する
[登録されているNon-Fungibleアイテムトークンの一覧を取得](https://docs.blockchain.line.me/ja/api-guide/category-item-tokens/retrieve#v1-item-tokens-contractId-non-fungibles-get)します

```src/index.ts
import * as dotenv from 'dotenv';
import * as devSdk from '@line/lbd-sdk-js';
import {
    WalletResponse,
    PageRequest,
    OrderBy,
    ItemTokenType,
} 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 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 getNFTInfo = async (contractId: string, tokenType: string): Promise<ItemTokenType[]|undefined> => {
  const pageRequest: PageRequest = {
    page: 1,
    limit: 10,
    orderBy: OrderBy.ASC 
  };
  const res = await httpClient.nonFungibleTokens(contractId, pageRequest);
  return res.responseData;
}

const main = async () => {
  const wallets = await getWallets();
  const btfWallet = getWalletByName('bathtimefish', wallets);
  if (!btfWallet) throw new Error('Wallet not found');
  const nft = await getNFTInfo(
    process.env.ITEM_TOKEN_CONTRACT_ID as string,
    process.env.ITEM_TOKEN_TYPE as string,
  );
  console.log(nft);
};

main();
```

実行すると以下のように登録されたNFTのリストが取得できます

```shell
yarn run ts-node src/index.ts
[
  {
    tokenType: '[NFT TOKEN TYPE]',
    name: 'BTFHOGEHOGE',
    meta: '',
    createdAt: 1664690394589,
    totalSupply: '0',
    totalMint: '0',
    totalBurn: '0'
  }
]
✨  Done in 1.93s.
```

# Non-fungibleアイテムトークンを鋳造する
[Non-fungibleアイテムトークンを鋳造を新しく鋳造](https://docs.blockchain.line.me/ja/api-guide/category-item-tokens/mint-burn#v1-item-tokens-contractId-non-fungibles-tokenType-mint-post)します。ここでは新規鋳造したNon-fungibleアイテムトークンをオーナーのウォレットに送ります

```src/index.ts
import * as dotenv from 'dotenv';
import * as devSdk from '@line/lbd-sdk-js';
import {
    WalletResponse,
    PageRequest,
    OrderBy,
    ItemTokenType,
    NonFungibleTokenType,
    NonFungibleTokenMintRequest,
    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 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 getNFTInfo = async (contractId: string, tokenType: string): Promise<ItemTokenType[]|undefined> => {
  const pageRequest: PageRequest = {
    page: 1,
    limit: 10,
    orderBy: OrderBy.ASC 
  };
  const res = await httpClient.nonFungibleTokens(contractId, pageRequest);
  return res.responseData;
}

const mintNFT = async (contractId: string, tokenType: string, walletAddress: string, name: string, meta: string): Promise<TxHashResponse|undefined> => {
  const mintRequest: NonFungibleTokenMintRequest = {
    ownerAddress: process.env.OWNER_WALLET_ADDRESS as string,
    ownerSecret: process.env.OWNER_WALLET_SECRET as string,
    toAddress: walletAddress,
    name,
    meta
  }
  const res = await httpClient.mintNonFungibleToken(contractId, tokenType, mintRequest);
  return res.responseData;
}

const main = async () => {
  const wallets = await getWallets();
  const btfWallet = getWalletByName('bathtimefish', wallets);
  if (!btfWallet) throw new Error('Wallet not found');
  const nft = await getNFTInfo(
    process.env.ITEM_TOKEN_CONTRACT_ID as string,
    process.env.ITEM_TOKEN_TYPE as string,
  );
  if (!nft || nft.length < 1) throw new Error('NFT not found');
  const minted = await mintNFT(
    process.env.ITEM_TOKEN_CONTRACT_ID as string,
    nft[0].tokenType,
    wallets[0].walletAddress,
    'BTFHOGEHOGE0101',
    'BathTimeFish presents awesome HOGEHOGE image',
  );
  console.log(minted);
};

main();
```

正常に処理されると[TxHash](https://docs.blockchain.line.me/ja/glossary/#transaction-hash-txhash)が返ります

```
yarn run ts-node src/index.ts
{
  txHash: '7B6978210A1A8CDB624DDE7FC01EF55A949AA5BD8E12AFD260043623BEC5CA64'
}
✨  Done in 2.40s.
```

# Non-fungibleアイテムトークンのtypeを取得する
[指定されたcontract ID、Token Typeで鋳造されたnon-fungibleアイテムトークンを取得](https://docs.blockchain.line.me/ja/api-guide/category-item-tokens/retrieve#v1-item-tokens-contractId-non-fungibles-tokenType-get)します。Non-fungibleアイテムトークンのtypeと鋳造されたtokenの一覧が取得できます

```src/index.ts
import * as dotenv from 'dotenv';
import * as devSdk from '@line/lbd-sdk-js';
import {
    WalletResponse,
    PageRequest,
    OrderBy,
    ItemTokenType,
    NonFungibleTokenType,
} 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 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 getNFTInfo = async (contractId: string, tokenType: string): Promise<ItemTokenType[]|undefined> => {
  const pageRequest: PageRequest = {
    page: 1,
    limit: 10,
    orderBy: OrderBy.ASC 
  };
  const res = await httpClient.nonFungibleTokens(contractId, pageRequest);
  return res.responseData;
};

const getNFTType = async (contractId: string, tokenType: string): Promise<NonFungibleTokenType|undefined> => {
  const pageRequest: PageRequest = {
    page: 1,
    limit: 10,
    orderBy: OrderBy.ASC 
  };
  const res = await httpClient.nonFungibleTokenType(contractId, tokenType, pageRequest);
  return res.responseData;
};


const main = async () => {
  const wallets = await getWallets();
  const btfWallet = getWalletByName('bathtimefish', wallets);
  if (!btfWallet) throw new Error('Wallet not found');
  const nft = await getNFTInfo(
    process.env.ITEM_TOKEN_CONTRACT_ID as string,
    process.env.ITEM_TOKEN_TYPE as string,
  );
  if (!nft || nft.length < 1) throw new Error('NFT not found');
  const nftType = await getNFTType(
    process.env.ITEM_TOKEN_CONTRACT_ID as string,
    nft[0].tokenType,
  );
  console.log(nftType);
};

main();
```

先ほど鋳造してウォレットに送ったNon-fungibleアイテムトークンも取得できています

```shell
{
  tokenType: '[TOKEN TYPE]',
  name: 'BTFHOGEHOGE',
  meta: '',
  createdAt: 1664690394589,
  totalSupply: '1',
  totalMint: '1',
  totalBurn: '0',
  token: [
    {
      tokenIndex: '[TOKEN INDEX]',
      name: 'BTFHOGEHOGE0101',
      meta: 'BathTimeFish presents awesome HOGEHOGE image',
      createdAt: 1664693560571,
      burnedAt: null
    }
  ]
}
✨  Done in 2.11s.
```

# Non-fungibleアイテムトークンを転送する

鋳造したアイテムトークンを`Alice`に転送します

ここでSDKにバグを発見しました。[response.d.tsの321行目]([この行](https://github.com/line/line-blockchain-developers-sdk-js/blob/17546da3d3ab55d2695ea6849ddfc30f49e130f0/lib/response.ts#L321))`NonFungibleTokenType`型の`tokens`を`token`に修正する必要があります。[プルリク送っておきました](https://github.com/line/line-blockchain-developers-sdk-js/pull/69)

```response.d.ts
export declare class NonFungibleTokenType {
    readonly name: string;
    readonly meta: string;
    readonly tokenType: string;
    readonly totalSupply: number;
    readonly totalMint: number;
    readonly totalBurn: number;
    readonly createdAt: number;
    // readonly tokens: Array<NonFungibleIndex>;
    readonly token: Array<NonFungibleIndex>;
    constructor(name: string, meta: string, tokenType: string, totalSupply: number, totalMint: number, totalBurn: number, createdAt: number, tokens: Array<NonFungibleIndex>);
}
```

修正した上で、転送処理は以下のコードになります

```src/index.ts
import * as dotenv from 'dotenv';
import * as devSdk from '@line/lbd-sdk-js';
import {
    WalletResponse,
    PageRequest,
    OrderBy,
    ItemTokenType,
    NonFungibleTokenType,
    TransferNonFungibleTokenRequest,
    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 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 getNFTInfo = async (contractId: string, tokenType: string): Promise<ItemTokenType[]|undefined> => {
  const pageRequest: PageRequest = {
    page: 1,
    limit: 10,
    orderBy: OrderBy.ASC 
  };
  const res = await httpClient.nonFungibleTokens(contractId, pageRequest);
  return res.responseData;
};

const getNFTType = async (contractId: string, tokenType: string): Promise<NonFungibleTokenType|undefined> => {
  const pageRequest: PageRequest = {
    page: 1,
    limit: 10,
    orderBy: OrderBy.ASC 
  };
  const res = await httpClient.nonFungibleTokenType(contractId, tokenType, pageRequest);
  return res.responseData;
};

const transferNFT = async (walletAddress: string, contractId: string, tokenType: string, tokenIndex: string, request: TransferNonFungibleTokenRequest): Promise<TxHashResponse|undefined> => {
  const res = await httpClient.transferNonFungibleTokenOfWallet(walletAddress, contractId, tokenType, tokenIndex, request);
  return res.responseData;
};

const main = async () => {
  const wallets = await getWallets();
  const btfWallet = getWalletByName('bathtimefish', wallets);
  if (!btfWallet) throw new Error('Wallet not found');
  const nft = await getNFTInfo(
    process.env.ITEM_TOKEN_CONTRACT_ID as string,
    process.env.ITEM_TOKEN_TYPE as string,
  );
  if (!nft || nft.length < 1) throw new Error('NFT not found');
  const nftType = await getNFTType(
    process.env.ITEM_TOKEN_CONTRACT_ID as string,
    nft[0].tokenType,
  );
  if (!nftType || nftType.token.length < 1) throw new Error('Token Index was not found');
  const aliceWallet = getWalletByName('Alice', wallets);
  if (!aliceWallet) throw new Error('Wallet not found');
  const transferRequest: TransferNonFungibleTokenRequest = {
    walletSecret: process.env.OWNER_WALLET_SECRET as string,
    toAddress: aliceWallet.walletAddress,
  };
  const transferd = await transferNFT(
    wallets[0].walletAddress,
    process.env.ITEM_TOKEN_CONTRACT_ID as string,
    nftType.tokenType,
    nftType.token[0].tokenIndex,
    transferRequest,
  );
  console.log(transferd);
};

main();
```

正常に処理されるとTxHashが返ります

```shell
yarn run ts-node src/index.ts
{
  txHash: '6EE0E2D774E215AA615093B0F18446F8E3C3A7A4F8BAF4EB92D73A4F7802BA0F'
}
✨  Done in 2.15s.
```

# Non-fungibleアイテムトークンの所有者を取得する
[特定のnon-fungibleアイテムトークンの保有者を取得](https://docs.blockchain.line.me/ja/api-guide/category-item-tokens/retrieve#v1-item-tokens-contractId-non-fungibles-tokenType-tokenIndex-holder-get)します

```src/index.ts
 * as dotenv from 'dotenv';
import * as devSdk from '@line/lbd-sdk-js';
import {
    WalletResponse,
    PageRequest,
    OrderBy,
    ItemTokenType,
    NonFungibleTokenType,
    NonFungibleTokenHolder,
} 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 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 getWalletByAddress = (address: string, wallets: WalletResponse[]): WalletResponse|undefined => {
  return wallets.find((w) => { return w.walletAddress === address });
};

const getNFTInfo = async (contractId: string, tokenType: string): Promise<ItemTokenType[]|undefined> => {
  const pageRequest: PageRequest = {
    page: 1,
    limit: 10,
    orderBy: OrderBy.ASC 
  };
  const res = await httpClient.nonFungibleTokens(contractId, pageRequest);
  return res.responseData;
};

const getNFTType = async (contractId: string, tokenType: string): Promise<NonFungibleTokenType|undefined> => {
  const pageRequest: PageRequest = {
    page: 1,
    limit: 10,
    orderBy: OrderBy.ASC 
  };
  const res = await httpClient.nonFungibleTokenType(contractId, tokenType, pageRequest);
  return res.responseData;
};

const getNFTHolder = async (contractId: string, tokenType: string, tokenIndex: string): Promise<NonFungibleTokenHolder|undefined> => {
  const res = await httpClient.nonFungibleTokenHolder(contractId, tokenType, tokenIndex);
  return res.responseData;
};

const main = async () => {
  const wallets = await getWallets();
  const btfWallet = getWalletByName('bathtimefish', wallets);
  if (!btfWallet) throw new Error('Wallet not found');
  const nft = await getNFTInfo(
    process.env.ITEM_TOKEN_CONTRACT_ID as string,
    process.env.ITEM_TOKEN_TYPE as string,
  );
  if (!nft || nft.length < 1) throw new Error('NFT not found');

  const nftType = await getNFTType(
    process.env.ITEM_TOKEN_CONTRACT_ID as string,
    nft[0].tokenType,
  );
  if (!nftType || nftType.token.length < 1) throw new Error('Token Index was not found');
  const aliceWallet = getWalletByName('Alice', wallets);
  if (!aliceWallet) throw new Error('Wallet not found');
  const holder = await getNFTHolder(
    process.env.ITEM_TOKEN_CONTRACT_ID as string,
    nftType.tokenType,
    nftType.token[0].tokenIndex,
  );
  if (!holder) throw new Error('Holder was not found');
  console.log(holder);
  const holdersWallet = getWalletByAddress(holder.walletAddress, wallets);
  console.log(`Holder's name: ${holdersWallet?.name}`);

};

main();
```

Non-fungibleアイテムトークンが所有者が無事`Alice`に転送されていることが確認できました

```shell
yarn run ts-node src/index.ts
{
  tokenId: '[TOKEN ID]',
  walletAddress: '[WALLET ADDRESS]',
  userId: null,
  amount: '1'
}
Holder's name: Alice
✨  Done in 2.09s.
```

# おわりに
JavaScript SDKを利用してNon-Fungibleアイテムトークンを生成したり転送したりしてみました。最後にアイテムトークンを焼却しようと思いましたが、ちょっと手順がややこしいので次回に回そうと思います。