ブラウザからSymbolブロックチェーンへファイル書き込みを行う


OPENSPHERE-Incさんが公開されているライブラリ、Metal on Symbolを使用してブラウザからバイナリデータを書き込んでみます。

https://github.com/OPENSPHERE-Inc/metal-on-symbol

> Metal（メタル）とは Symbol ブロックチェーンに、任意の（サイズの）データを書き込んだり読み込んだりするためのプロトコルです。 簡単に言えば、Symbol ブロックチェーンをオンラインの不揮発性メモリ（ROM）として使用できます。

これをbrowserify化したものが以下になります。

https://github.com/xembook/symbol-browserify/blob/main/metal-on-symbol-0.2.2.js

作成方法は以下の通りです。

```sh
npm install metal-on-symbol
browserify -r ./node_modules/metal-on-symbol  -o metal-on-symbol-0.2.2.js 
```


今回はこのmetal-on-symbol-0.2.2.jsを利用してブラウザからSymbolブロックチェーンへファイルをアップロードしてみます。

以下のような画面を作るのが今回の目標です。
メリークリスマス！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29187/a1a1719d-e87c-2423-3da1-1c1e91bca94a.png)

#### 注意事項

今回は動作検証なので以下の情報については決め打ちで指定しています。

- 接続ノード
    - テストネット
- 秘密鍵

まずはHTMLの構造定義です。

```html
<!doctype html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<title>metal on browser</title>
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://xembook.github.io/symbol-browserify/metal-on-symbol-0.2.2.js"></script>
<script src="https://xembook.github.io/nem2-browserify/symbol-sdk-2.0.3.js"></script>
<script>
</script>
  </head>
  <body>
	<nav class="navbar navbar-dark bg-dark mb-3">
	  <a class="navbar-brand ms-3" href="#">Metal on Browser</a>
	</nav>
	<div class="container">
	  <form>
		<div class="mb-3 d-flex">
		  <input type="file" class="form-control me-2" id="uploadImage" />
		  <button type="button" class="btn btn-primary form-control w-25" onclick="forgeImage()">forge</button>
		</div>
		<div class="mb-3 d-flex">
		  <input type="text" class="form-control me-2" id="metalid" />
		  <button type="button" class="btn btn-primary form-control w-25" onclick="fetchImage()">fetch</button>　
		  <button type="button" class="btn btn-primary form-control w-25" onclick="scrapImage()">scrap</button>
		</div>
		<div id="uploadImageArea"></div>
	  </form>
	</div>
  </body>
</html>

```

jqueryと以下の2ファイルを読み込んでおきます。
- metal-on-symbol-0.1.17.js
- symbol-sdk-2.0.3.js

ボタンは3種類で以下のファンクションを割り当てます。
- forge
    - forgeImage()
- fetch
    - fetchImage()
- scrap
    - scrapImage()


ではスクリプトを仕込んでいきましょう。

# 初期設定

各種ライブラリを読み込み、接続するノードとWebSocketの設定、ファイルアップロードを行うアカウントの設定を行います。

```js
const sym = require("/node_modules/symbol-sdk");
const Buffer = require("/node_modules/buffer").Buffer;
const metal = require("/node_modules/metal-on-symbol");

const ss = new metal.SymbolService({
    node_url: "https://mikun-testnet.tk:3001" ,
    repo_factory_config:{
        websocketUrl:"wss://mikun-testnet.tk:3001/ws",
        websocketInjected:WebSocket
    }
});

const ms = new metal.MetalService(ss);

alice = sym.Account.createFromPrivateKey("896E43895B908AF5847ECCB2645543751D94BD87E71058B003417FED512*****",152);

```

ノードについて今回は `mikun-testnet.tk`をお借りしましたが、すこし大きめの負荷がかかりますので、以下のnode listから同期が追いついているものを選んで接続しましょう。

https://symbolnodes.org/nodes_testnet/


# ファイルアップロード

ブラウザで指定したファイルをSymbolブロックチェーンへアップロードします。
MetalIDを計算しておき画面に表示します。

```js:forgeImage
async function forgeImage() {
	const uploadImage = document.querySelector('#uploadImage')
	const file = uploadImage.files[0]
	const reader = new FileReader()
	reader.onload = async (event) => {

		const forgedTxs = await ms.createForgeTxs(
			sym.MetadataType.Account, 
			alice.publicAccount,
			alice.publicAccount,
			undefined,
			event.currentTarget.result
		); 
		
		const metalId = metal.MetalService.calculateMetalId(
			sym.MetadataType.Account, 
			alice.address,
			alice.address,
			undefined,
			forgedTxs.key
		); 

		const element = document.getElementById('metalid');
		element.value = metalId;

		const batches = await ss.buildSignedAggregateCompleteTxBatches(
			forgedTxs.txs,
			alice,
			undefined,
		);

		const errors = await ss.executeBatches(batches, alice);
	}
	reader.readAsArrayBuffer(file);
}
```



# ファイルの読み込み

Metal IDをキーにSymbolブロックチェーンへアップロードしたファイルを読み込みます。

```js:fetchImage
async function fetchImage() {

	const element = document.getElementById('metalid');
	const data = await ms.fetchByMetalId (element.value); 
	const imgblob = new Blob([data.payload],{type:"image/jpeg"});
	fileUrl = URL.createObjectURL(imgblob) ;
	document.querySelector('#uploadImageArea').innerHTML = `<img src="${fileUrl}" width="100%" />`;
}
```

# ファイル削除

Metal IDに紐づけられたファイルの紐づけを削除します。

```js:scrapImage
async function scrapImage() {

	const element = document.getElementById('metalid');
	const metadataEntry = (await ms.getFirstChunk(element.value)).metadataEntry;

	const txs = await ms.createScrapTxs(
		metadataEntry.type,
		alice.publicAccount,
		alice.publicAccount,
		metadataEntry.targetId,
		metadataEntry.scopedMetadataKey,
	);

	const batches = await ss.buildSignedAggregateCompleteTxBatches(
		txs,
		alice,
		undefined,
	);

	const errors = await ss.executeBatches(batches, alice);
}
```

# 全ソースコード

```html
<!doctype html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<title>metal on browser</title>
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://xembook.github.io/symbol-browserify/metal-on-symbol-0.2.2.js"></script>
<script src="https://xembook.github.io/nem2-browserify/symbol-sdk-2.0.3.js"></script>
<script>
const sym = require("/node_modules/symbol-sdk");
const Buffer = require("/node_modules/buffer").Buffer;
const metal = require("/node_modules/metal-on-symbol");

const ss = new metal.SymbolService({
    node_url: "https://mikun-testnet.tk:3001" ,
    repo_factory_config:{
        websocketUrl:"wss://mikun-testnet.tk:3001/ws",
        websocketInjected:WebSocket
    }
});

const ms = new metal.MetalService(ss);

alice = sym.Account.createFromPrivateKey("896E43895B908AF5847ECCB2645543751D94BD87E71058B003417FED512*****",152);

async function forgeImage() {
	const uploadImage = document.querySelector('#uploadImage')
	const file = uploadImage.files[0]
	const reader = new FileReader()
	reader.onload = async (event) => {

		const forgedTxs = await ms.createForgeTxs(
			sym.MetadataType.Account, 
			alice.publicAccount,
			alice.publicAccount,
			undefined,
			event.currentTarget.result
		); 
		
		const metalId = metal.MetalService.calculateMetalId(
			sym.MetadataType.Account, 
			alice.address,
			alice.address,
			undefined,
			forgedTxs.key
		); 

		const element = document.getElementById('metalid');
		element.value = metalId;

		const batches = await ss.buildSignedAggregateCompleteTxBatches(
			forgedTxs.txs,
			alice,
			undefined,
		);

		const errors = await ss.executeBatches(batches, alice);
	}
	reader.readAsArrayBuffer(file);
}

async function fetchImage() {

	const element = document.getElementById('metalid');
	const data = await ms.fetchByMetalId (element.value); 
	const imgblob = new Blob([data.payload],{type:"image/jpeg"});
	fileUrl = URL.createObjectURL(imgblob) ;
	document.querySelector('#uploadImageArea').innerHTML = `<img src="${fileUrl}" width="100%" />`;
}

async function scrapImage() {

	const element = document.getElementById('metalid');
	const metadataEntry = (await ms.getFirstChunk(element.value)).metadataEntry;

	const txs = await ms.createScrapTxs(
		metadataEntry.type,
		alice.publicAccount,
		alice.publicAccount,
		metadataEntry.targetId,
		metadataEntry.scopedMetadataKey,
	);

	const batches = await ss.buildSignedAggregateCompleteTxBatches(
		txs,
		alice,
		undefined,
	);

	const errors = await ss.executeBatches(batches, alice);
}

</script>
  </head>
  <body>
	<nav class="navbar navbar-dark bg-dark mb-3">
	  <a class="navbar-brand ms-3" href="#">Metal on Browser</a>
	</nav>
	<div class="container">
	  <form>
		<div class="mb-3 d-flex">
		  <input type="file" class="form-control me-2" id="uploadImage" />
		  <button type="button" class="btn btn-primary form-control w-25" onclick="forgeImage()">forge</button>
		</div>
		<div class="mb-3 d-flex">
		  <input type="text" class="form-control me-2" id="metalid" />
		  <button type="button" class="btn btn-primary form-control w-25" onclick="fetchImage()">fetch</button>　
		  <button type="button" class="btn btn-primary form-control w-25" onclick="scrapImage()">scrap</button>
		</div>
		<div id="uploadImageArea"></div>
	  </form>
	</div>
  </body>
</html>
```